Skip to content

Add pure and immutable keywords to ensure code has no unintended side-effects #17181

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).

purefunctionx(arg){returnarg}

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:
purefunctionx(){constarr=[]arr.push(1)returnarr}
    • 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
purefunctionfn(arg : ('a'|'b')[]){}immutablex=['a','b']fn(x)

With objects/interfaces

The keyword(s) should also be allowed in object (and by extension interface) definitions:

constobj1={// immutable and non-pureimmutablefn1: function(){},// mutable and purefn2: purefunction(){},// immutable and pureimmutablefn3: purefunction(){},// immutable and non-pureimmutablefn3(){},// immutable and purepurefn3(){},}interfaceIFace{puretoString() : string// pure must always return a valueimmutableprop : numberimmutablepurefrozenFn() : boolean}

With existing typings

With this proposal, the base javascript typings could be updated to support it.
I.e. the array interface would become:

interfaceArray<T>{puretoString(): string;puretoLocaleString(): string;pureconcat(...items: T[][]): T[];pureconcat(...items: (T|T[])[]): T[];purejoin(separator?: string): string;pureindexOf(searchElement: T,fromIndex?: number): number;purelastIndexOf(searchElement: T,fromIndex?: number): number;pureevery(callbackfn: (this: void,value: T,index: number,array: T[])=>boolean): boolean;pureevery(callbackfn: (this: void,value: T,index: number,array: T[])=>boolean,thisArg: undefined): boolean;pureevery<Z>(callbackfn: (this: Z,value: T,index: number,array: T[])=>boolean,thisArg: Z): boolean;puresome(callbackfn: (this: void,value: T,index: number,array: T[])=>boolean): boolean;puresome(callbackfn: (this: void,value: T,index: number,array: T[])=>boolean,thisArg: undefined): boolean;puresome<Z>(callbackfn: (this: Z,value: T,index: number,array: T[])=>boolean,thisArg: Z): boolean;pureforEach(callbackfn: (this: void,value: T,index: number,array: T[])=>void): void;pureforEach(callbackfn: (this: void,value: T,index: number,array: T[])=>void,thisArg: undefined): void;pureforEach<Z>(callbackfn: (this: Z,value: T,index: number,array: T[])=>void,thisArg: Z): void;puremap<U>(this: [T,T,T,T,T],callbackfn: (this: void,value: T,index: number,array: T[])=>U): [U,U,U,U,U];puremap<U>(this: [T,T,T,T,T],callbackfn: (this: void,value: T,index: number,array: T[])=>U,thisArg: undefined): [U,U,U,U,U];puremap<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];puremap<U>(this: [T,T,T,T],callbackfn: (this: void,value: T,index: number,array: T[])=>U): [U,U,U,U];puremap<U>(this: [T,T,T,T],callbackfn: (this: void,value: T,index: number,array: T[])=>U,thisArg: undefined): [U,U,U,U];puremap<Z,U>(this: [T,T,T,T],callbackfn: (this: Z,value: T,index: number,array: T[])=>U,thisArg: Z): [U,U,U,U];puremap<U>(this: [T,T,T],callbackfn: (this: void,value: T,index: number,array: T[])=>U): [U,U,U];puremap<U>(this: [T,T,T],callbackfn: (this: void,value: T,index: number,array: T[])=>U,thisArg: undefined): [U,U,U];puremap<Z,U>(this: [T,T,T],callbackfn: (this: Z,value: T,index: number,array: T[])=>U,thisArg: Z): [U,U,U];puremap<U>(this: [T,T],callbackfn: (this: void,value: T,index: number,array: T[])=>U): [U,U];puremap<U>(this: [T,T],callbackfn: (this: void,value: T,index: number,array: T[])=>U,thisArg: undefined): [U,U];puremap<Z,U>(this: [T,T],callbackfn: (this: Z,value: T,index: number,array: T[])=>U,thisArg: Z): [U,U];puremap<U>(callbackfn: (this: void,value: T,index: number,array: T[])=>U): U[];puremap<U>(callbackfn: (this: void,value: T,index: number,array: T[])=>U,thisArg: undefined): U[];puremap<Z,U>(callbackfn: (this: Z,value: T,index: number,array: T[])=>U,thisArg: Z): U[];purefilter(callbackfn: (this: void,value: T,index: number,array: T[])=>any): T[];purefilter(callbackfn: (this: void,value: T,index: number,array: T[])=>any,thisArg: undefined): T[];purefilter<Z>(callbackfn: (this: Z,value: T,index: number,array: T[])=>any,thisArg: Z): T[];purereduce(callbackfn: (previousValue: T,currentValue: T,currentIndex: number,array: T[])=>T,initialValue?: T): T;purereduce<U>(callbackfn: (previousValue: U,currentValue: T,currentIndex: number,array: T[])=>U,initialValue: U): U;purereduceRight(callbackfn: (previousValue: T,currentValue: T,currentIndex: number,array: T[])=>T,initialValue?: T): T;purereduceRight<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;}

Metadata

Metadata

Assignees

No one assigned

    Labels

    In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions