Description
The aim of this proposal is to add some immutability and pure checking into the typescript compiler. The proposal adds two new keywords that would give developers a means to define functions that are pure - meaning that the function has no-side effects, and define variables that are immutable - meaning that they can never be used in an impure context.
Pure
The pure
keyword is used to define a function with no side-effects (it is allowed in the same places as the async
keyword).
pure function x(arg) {
return arg
}
The keyword should be not be emitted into compiled javascript code.
A pure function:
- May not call an function not tagged as pure in the context of arguments, or this.
this.nonPure()
,arg.nonPure()
,nonPure(arg)
, andnonPure(this)
are all disallowed.
- May make non-pure calls on instance variables, as the non-pureness applies to instance variables, so the function is technically still side-effect free:
pure function x() {
const arr = []
arr.push(1)
return arr
}
-
- this one i'm not entirely sure about, but it seems like it would be very hard to build pure functions without it.
-
- languages like elm get around this by having a push function which returns a new array.
- Modify the input variables
arg.x = 1
is disallowed within the function body.
- Modify variables on
this
this.y = 1
is disallowed within the function body.
- Must return a value (otherwise there's no point to the function!).
Immutable
Similarly a variable may be tagged as immutable:
immutable x = []
(maybe keyword should be shortened to immut
, or the pure
keyword could be reused for consistency?).
This keyword is replaced with const
in emitted code.
An immutable variable:
- Is treated as if it were
const
(i.e. its reference may not be reassigned). - May not have any impure instance methods called on it.
x.nonPure()
is disallowed.
- May not be passed as an argument to impure functions.
nonPure(x)
is disallowed.
- May not be reassigned to a variable reference that is also not marked as pure.
const y = x;
is disallowed.const y = { z: x }
is disallowed.
- May not have instance properties set on it.
x.foo = 1
is disallowed.
- May be passed to pure functions.
pureFn(x)
is allowed.
- May have pure instance methods called on it.
x.toString()
is allowed.
- For arrays, its type is strictly set at definition time, meaning that element-wise type checks will pass (fixes: Type checking element typed arrays #16389)
- i.e. the following code will now pass
pure function fn(arg : ('a' | 'b')[]) { }
immutable x = ['a', 'b']
fn(x)
With objects/interfaces
The keyword(s) should also be allowed in object
(and by extension interface
) definitions:
const obj1 = {
// immutable and non-pure
immutable fn1: function () { },
// mutable and pure
fn2: pure function () { },
// immutable and pure
immutable fn3: pure function () { },
// immutable and non-pure
immutable fn3() { },
// immutable and pure
pure fn3() { },
}
interface IFace {
pure toString() : string // pure must always return a value
immutable prop : number
immutable pure frozenFn() : boolean
}
With existing typings
With this proposal, the base javascript typings could be updated to support it.
I.e. the array interface would become:
interface Array<T> {
pure toString(): string;
pure toLocaleString(): string;
pure concat(...items: T[][]): T[];
pure concat(...items: (T | T[])[]): T[];
pure join(separator?: string): string;
pure indexOf(searchElement: T, fromIndex?: number): number;
pure lastIndexOf(searchElement: T, fromIndex?: number): number;
pure every(callbackfn: (this: void, value: T, index: number, array: T[]) => boolean): boolean;
pure every(callbackfn: (this: void, value: T, index: number, array: T[]) => boolean, thisArg: undefined): boolean;
pure every<Z>(callbackfn: (this: Z, value: T, index: number, array: T[]) => boolean, thisArg: Z): boolean;
pure some(callbackfn: (this: void, value: T, index: number, array: T[]) => boolean): boolean;
pure some(callbackfn: (this: void, value: T, index: number, array: T[]) => boolean, thisArg: undefined): boolean;
pure some<Z>(callbackfn: (this: Z, value: T, index: number, array: T[]) => boolean, thisArg: Z): boolean;
pure forEach(callbackfn: (this: void, value: T, index: number, array: T[]) => void): void;
pure forEach(callbackfn: (this: void, value: T, index: number, array: T[]) => void, thisArg: undefined): void;
pure forEach<Z>(callbackfn: (this: Z, value: T, index: number, array: T[]) => void, thisArg: Z): void;
pure map<U>(this: [T, T, T, T, T], callbackfn: (this: void, value: T, index: number, array: T[]) => U): [U, U, U, U, U];
pure map<U>(this: [T, T, T, T, T], callbackfn: (this: void, value: T, index: number, array: T[]) => U, thisArg: undefined): [U, U, U, U, U];
pure map<Z, U>(this: [T, T, T, T, T], callbackfn: (this: Z, value: T, index: number, array: T[]) => U, thisArg: Z): [U, U, U, U, U];
pure map<U>(this: [T, T, T, T], callbackfn: (this: void, value: T, index: number, array: T[]) => U): [U, U, U, U];
pure map<U>(this: [T, T, T, T], callbackfn: (this: void, value: T, index: number, array: T[]) => U, thisArg: undefined): [U, U, U, U];
pure map<Z, U>(this: [T, T, T, T], callbackfn: (this: Z, value: T, index: number, array: T[]) => U, thisArg: Z): [U, U, U, U];
pure map<U>(this: [T, T, T], callbackfn: (this: void, value: T, index: number, array: T[]) => U): [U, U, U];
pure map<U>(this: [T, T, T], callbackfn: (this: void, value: T, index: number, array: T[]) => U, thisArg: undefined): [U, U, U];
pure map<Z, U>(this: [T, T, T], callbackfn: (this: Z, value: T, index: number, array: T[]) => U, thisArg: Z): [U, U, U];
pure map<U>(this: [T, T], callbackfn: (this: void, value: T, index: number, array: T[]) => U): [U, U];
pure map<U>(this: [T, T], callbackfn: (this: void, value: T, index: number, array: T[]) => U, thisArg: undefined): [U, U];
pure map<Z, U>(this: [T, T], callbackfn: (this: Z, value: T, index: number, array: T[]) => U, thisArg: Z): [U, U];
pure map<U>(callbackfn: (this: void, value: T, index: number, array: T[]) => U): U[];
pure map<U>(callbackfn: (this: void, value: T, index: number, array: T[]) => U, thisArg: undefined): U[];
pure map<Z, U>(callbackfn: (this: Z, value: T, index: number, array: T[]) => U, thisArg: Z): U[];
pure filter(callbackfn: (this: void, value: T, index: number, array: T[]) => any): T[];
pure filter(callbackfn: (this: void, value: T, index: number, array: T[]) => any, thisArg: undefined): T[];
pure filter<Z>(callbackfn: (this: Z, value: T, index: number, array: T[]) => any, thisArg: Z): T[];
pure reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue?: T): T;
pure reduce<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U;
pure reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue?: T): T;
pure reduceRight<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U;
push(...items: T[]): number;
pop(): T | undefined;
reverse(): T[];
shift(): T | undefined;
slice(start?: number, end?: number): T[];
sort(compareFn?: (a: T, b: T) => number): this;
splice(start: number, deleteCount?: number): T[];
splice(start: number, deleteCount: number, ...items: T[]): T[];
unshift(...items: T[]): number;
[n: number]: T;
}