microsoft / microsoft/TypeScript

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

オープン
#17,181 コメント 20 件 リアクション 72 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

In Discussion Suggestion
主要言語
Go
スター
111k
フォーク
14.4k
平均マージ
1日 19時間
マージ済み PR(30日)
117

説明

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;
}

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

この提案には、ソースファイル、テスト、エントリーポイントの記載がありません。まず、キーワードの解析、純粋性と不変性のチェック、JavaScript の出力、ライブラリの型定義の更新に関係する TypeScript コンパイラーの領域を洗い出してください。完了とするには、合意された設計、それらの領域全体にわたる実装、提案されたルールと出力結果を対象とするテストが必要です。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
javascript, typescript
領域
compilers
issue の種類
機能追加
難易度
5/5
見積もり時間
1週間以上
活発さ
停滞
明瞭さ
説明が足りない
初心者へのやさしさ
18/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。