question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

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

See original GitHub issue

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), 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:
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
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;
}

Issue Analytics

  • State:open
  • Created 6 years ago
  • Reactions:52
  • Comments:19 (9 by maintainers)

github_iconTop GitHub Comments

7reactions
bradzachercommented, Sep 13, 2021

An example of an implementation of a “purity” system is the contexts and capabilities system built into Hack: https://docs.hhvm.com/hack/contexts-and-capabilities/available-contexts-and-capabilities This system is more flexible than just my “disallow impure actions” proposal.

In the hack system you can opt a function into this system by adding [] before the return annotation

- function foo(): void {
+ function foo()[]: void {
    // ...
  }

This declares the function as having no capabilities - it has to be completely pure. Within the brackets you can include the name of a “context”. Contexts declare what “capabilities” a function can have (for example - can it write properties, or can it do IO operations).

A function with capabilities can only call other functions with the same capabilities. This system is leveraged heavily at Facebook to ensure that codegen pipelines are consistent and stable.

6reactions
bradzachercommented, Jul 14, 2017

How is it a duplicate of #13721? That issue is entirely about adding a comment to emitted code so that uglyifyjs can optimise it away? This is about adding features to the typescript language, pre compilation. In fact it emits no different code.

Similarly for #6532, that issue only pertains to reference assignment. You can still mess with the underlying object, which is the entire problem that I am attempting to solve here.

The problem is that there is no way to do compile time checks to ensure that an object has not been modified (which causes issues such as #16389 where the compiler cannot easily be sure that an object has been unmodified).

Read more comments on GitHub >

github_iconTop Results From Across the Web

What are side effects, and what you can do about them - JS.dev
You may have even come across the functional programming aficinado, who has claimed that no side effect code will save the day, and...
Read more >
Immutability — No Mutants Allowed | by Ghost
1. Less side-effects: a side-effect is when a function changes data outside of its own scope. The concept of a pure function is...
Read more >
Immutable vs mutable: Definitions, benefits & practical tips
Mutable vs immutable: what is immutability? ... So, what is immutability? To answer that, let's define what it is and isn't. ... In...
Read more >
The Complete Guide to Immutability in TypeScript
Calling an immutable function exhibits no side effects (no ... Debugging or simple reasoning about such code, without ensuring first that ...
Read more >
Data immutability
Here, user is an object, i.e. a non-primitive type, so we can freely mutate its properties. There are no exceptions thrown by the...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found