TypeScript v4 まとめ

8 min read読了の目安(約7300字

前回のv3に関する記事でこれまでの変遷を確認したので、ようやく本題のv4についてまとめていきます。

追加された新機能

【v4.0】 Variadic Tuple Types

function concat(arr1, arr2) {
    return [...arr1, ...arr2];
}
function tail(arg) {
    const [_, ...result] = arg;
    return result
}

function concat(arr1: [], arr2: []): [];
function concat<A>(arr1: [A], arr2: []): [A];
function concat<A, B>(arr1: [A, B], arr2: []): [A, B];
function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];
function concat<A, B, C, D>(arr1: [A, B, C, D], arr2: []): [A, B, C, D];
function concat<A, B, C, D, E>(arr1: [A, B, C, D, E], arr2: []): [A, B, C, D, E];
function concat<A, B, C, D, E, F>(arr1: [A, B, C, D, E, F], arr2: []): [A, B, C, D, E, F];)

従来までのTypeScriptでは、上記の concattail のように配列を結合する関数の型を表す場合、オーバーロードを行うことしか出来ず、特定のパターンにしか対応出来ません。

v4ではこのような問題に対処するために、2つの変更が加えられました。

1つ目はタプル型をスプレッドした型をそのまま型の表現に使用できるようになった点です。

function tail<T extends any[]>(arr: readonly [any, ...T]) {
    const [_ignored, ...rest] = arr;
    return rest;
}

const myTuple = [1, 2, 3, 4] as const;
const myArray = ["hello", "world"];

// type [2, 3, 4]
const r1 = tail(myTuple);

// type [2, 3, 4, ...string[]]
const r2 = tail([...myTuple, ...myArray] as const);

オーバーロードを行い、関数に渡す値の型を気にせずとも、正しい型推論が効きます。

また、2つ目の変更は、スプレッドしたタプル型をタプル型の宣言の中のどこでも使用できるようになった点です。

type Strings = [string, string];
type Numbers = [number, number];

// [string, string, ...Array<number | boolean>]
type StrStrNumNumBool = [...Strings, ...Numbers, boolean];

注意点として、型が取り込まれて結合されてしまいます。

上記の2つの変更により、 concat は下記のように書けます。

type Arr = readonly any[];

function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] {
    return [...arr1, ...arr2];
}

また、 partially apply argumentsなど関数を結合するようなパターンで、より Variadic Tuple Typesはより効果を発揮します。

type Arr = readonly unknown[];

function partialCall<T extends Arr, U extends Arr, R>(
    f: (...args: [...T, ...U]) => R, ...headArgs: T
) {
    return (...tailArgs: U) => f(...headArgs, ...tailArgs)
}

const foo = (x: string, y: number, z: boolean) => {}

// This doesn't work because we're feeding in the wrong type for 'x'.
const f1 = partialCall(foo, 100);
//                          ~~~
// error! Argument of type 'number' is not assignable to parameter of type 'string'.


// This doesn't work because we're passing in too many arguments.
const f2 = partialCall(foo, "hello", 100, true, "oops")
//                                              ~~~~~~
// error! Expected 4 arguments, but got 5.


// This works! It has the type '(y: number, z: boolean) => void'
const f3 = partialCall(foo, "hello");

f3(123, true);
// works!

f3();
// error! Expected 2 arguments, but got 0.

f3(123, "hello");
//      ~~~~~~~
// error! Argument of type 'string' is not assignable to parameter of type 'boolean'.

【v4.0】 Labeled Tuple Elements

タプル型の各要素に関して、ラベルを当てられるようになりました。

type Foo = [first: number, second?: string, ...rest: any[]];

しかし、1つの要素にラベルを当てた場合、全ての要素にラベルを当てなければなりません。

type Bar = [first: string, number];
//                         ~~~~~~
// error! Tuple members must all have names or all not have names.

【v4.0】 Class Property Inference from Constructors

class Square {
    // Previously: implicit any!
    // Now: inferred to `number`!
    area;
    sideLength;

    constructor(sideLength: number) {
        this.sideLength = sideLength;
        this.area = sideLength ** 2;
    }
}

クラスのプロパティの型がコンストラクターでの値を見て、当てられるようになりました。

class Square {
    sideLength;

    constructor(sideLength: number) {
        if (Math.random()) {
            this.sideLength = sideLength;
        }
    }

    get area() {
        return this.sideLength ** 2;
        //     ~~~~~~~~~~~~~~~
        // error! Object is possibly 'undefined'.
    }
}

全ての場合で変数が当てられなかった場合、undefinedも当てられます。

【v4.0】 Short-Circuiting Assignment Operators

ECMAScriptの新しい機能をサポートし、新しく &&=, ||=, ??= の代入演算子を追加しました。

// Before
(values ?? (values = [])).push("hello");

// After
(values ??= []).push("hello");

valuesがnullまたundefinedの場合は空の配列がvaluesに代入されます。

【v4.0】 unknown on catch Clause Bindings

try {
    // ...
}
catch (e: unknown) {
    // error!
    // Property 'toUpperCase' does not exist on type 'unknown'.
    console.log(e.toUpperCase());

    if (typeof e === "string") {
        // works!
        // We've narrowed 'e' down to the type 'string'.
        console.log(e.toUpperCase());
    }
}

これまでcatch句の変数はanyと型づけされており、バグの温床になっていました。
v4ではunknownを指定できるようになり、型の判定を行わないとメソッドを実行出来ないように制限をかけられるようになりました。

【v4.1】 Template Literal Types

type Color = "red" | "blue";
type Quantity = "one" | "two";

type SeussFish = `${Quantity | Color} fish`;
// same as
//   type SeussFish = "one fish" | "two fish"
//                  | "red fish" | "blue fish";

type VerticalAlignment = "top" | "middle" | "bottom";
type HorizontalAlignment = "left" | "center" | "right";

type Position = `${VerticalAlignment}-${HorizontalAlignment}`

テンプレートリテラルを型宣言で使用できるようになりました。

また、下記のように、ジェネリクスを通して、引数の型に使用することも可能です。

type PropEventSource<T> = {
    on<K extends string & keyof T>
        (eventName: `${K}Changed`, callback: (newValue: T[K]) => void ): void;
};

declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;

let person = makeWatchedObject({
    firstName: "Homer",
    age: 42,
    location: "Springfield",
});

// works! 'newName' is typed as 'string'
person.on("firstNameChanged", newName => {
    // 'newName' has the type of 'firstName'
    console.log(`new name is ${newName.toUpperCase()}`);
});

// works! 'newAge' is typed as 'number'
person.on("ageChanged", newAge => {
    if (newAge < 0) {
        console.log("warning! negative age");
    }
})

【v4.1】 Key Remapping in Mapped Types

// Remove the 'kind' property
type RemoveKindField<T> = {
    [K in keyof T as Exclude<K, "kind">]: T[K]
};

interface Circle {
    kind: "circle";
    radius: number;
}

type KindlessCircle = RemoveKindField<Circle>;
// same as
//   type KindlessCircle = {
//       radius: number;
//   };

as句で既存のMapped typeを使用することで、新しいMapped typeのkeyの型を指定できるようになりました。

【v4.2】 Smarter Type Alias Preservation

export type BasicPrimitive = number | string | boolean;

export function doStuff(value: BasicPrimitive) {
    if (Math.random() < 0.5) {
        return undefined;
    }

    return value;
}

従来のバージョンでは、 doStuff の返り値の型は number | string | boolean | undefinedとなっていました。
しかし、v4.2では、型がどのように宣言されたかタイムラインに沿って、追跡しており、BasicPrimitive | undefined と帰るようになりました。

【v4.2】 abstract Construct Signatures

abstract class Shape {
    abstract getArea(): number;
}

// Error! Can't instantiate an abstract class.
new Shape();

class Square extends Shape {
    #sideLength: number;

    constructor(sideLength: number) {
        this.#sideLength = sideLength;
    }

    getArea() {
        return this.#sideLength ** 2;
    }
}

// Works fine.
new Square(42);

abstractをつけることで抽象クラスとして宣言できるようになり、拡張のみでしか使えないように出来ます。

まとめ

v4.0ではタプル型を強化することにフォーカスされています。
スプレッドした型をそのまま型変数として扱えるようになったことで、複雑なケースにも対応できるようになりました。

それに留まらず、Template Literal Typesの導入に見られるように型の表現力がまた上がり、痒いところにますます手が届くようになっていきそうです。