Repository: TypeScript
Issue: Add pure and immutable keywords to ensure code has no unintended side-effects
Labels: Suggestion,In Discussion

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](https://tc39.github.io/ecma262/#sec-async-function-definitions)).
```TS
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)`, and `nonPure(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:
```JS
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: https://github.com/Microsoft/TypeScript/issues/16389)
  - i.e. the following code will now pass
```TS
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:
```TS
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:
```TS
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;
}
```