コンテンツにスキップ

[TypeScript] 2.8

TypeScript2.8のリリース内容まとめ

Conditional Types

HAD BETTER NORMAL

他の型が満たす条件によって、型の切り替えができる。
T extends U ? X : Y と書いて UにTを割り当て可能ならX、そうでなければY 型 という意味。

JavaScriptで書かれたある規則に従って動く関数のInterfaceを明確にするもの という位置づけだと個人的には思っている。

以下のような国籍と、そのアシスタントを定義した場合..

interface Japanese {
  myNo: number;
  name: string;
}

interface American {
  name: string;
}

class JapaneseAssistant {
  private _japaneseAssistantBrand!: never;
  hello(human: Japanese): void {
    console.log(`こんにちは。${human.name}さん`);
  }
}

class AmericanAssistant {
  private _americanAssistantBrand!: never;
  hello(human: American): void {
    console.log(`Hello! ${human.name}!`);
  }
}

国籍から最適なアシスタントを判定するConditional Type Assistant<T> の実装は以下。

type Assistant<T> = T extends Japanese
  ? JapaneseAssistant
  : T extends American
  ? AmericanAssistant
  : unknown;

第1引数として指定された国籍から、予想されるアシスタント型を第2引数で推論するassist関数の実装は以下。

function assist<T extends Japanese | American>(
  human: T,
  assistant: Assistant<T>
) {
  // javascriptで頑張っている実装とかがあるはず..
}

tscやIDEで推論およびエラーを出してくれる。

const beaton: American = { name: "beaton" };

// 第1引数にAmerican型のObjectを入れることで、第2引数はAmericanAssistantとみなされる
assist(beaton, new AmericanAssistant());

// 第2引数にJapaneseAssistantを入れるとエラーになる
assist(beaton, new JapaneseAssistant());

Distributive conditional types

HAD BETTER HARD

Conditional typesにUnion typesが指定された場合、適切に分散する。
具体的には、Union typesで指定されたそれぞれの型について条件判定を行う。

// T1はstringとbooleanそれぞれに対して↑の判定が行われる
// stringは"STR"、booleanは"BOOL"のため "STR" | "BOOL" 型と判定される
type T1 = TypeName<string | boolean>

TypeName<T>の定義は以下の通り。

type TypeName<T> = T extends string
  ? "STR"
  : T extends number
  ? "NUM"
  : T extends boolean
  ? "BOOL"
  : T extends undefined
  ? "NONE"
  : T extends Function
  ? "FUNC"
  : "OBJ";

Conditional typesは再帰を許容しない。

確かどこかのバージョンで許容するようになったはず。。

いくつかサンプルコードを紹介する。

アサイン可能な型のみフィルターするFilter型

type Filter<T, U> = T extends U ? T : never;

TがUnion Typesのとき、Filter<T, U>はUにアサイン可能な型のみをTから抽出した型である。
たとえば、以下のT1booleanとなる。

type T1 = Filter<number | string | boolean, Boolean | object>
// T1 = boolean

Tに指定されたUnion typesの型(number | string | boolean)はそれぞれについてConditional typesとして判定されるため

T U T extends U ? 結果
number Boolean │ object false never
string Boolean │ object false never
boolean Boolean │ object true boolean

よって boolean になる。

アサイン不可能な型のみ抽出するDiff型

Filter型の逆。

type Diff<T, U> = T extends U ? never : T;

TがUnion Typesのとき、Diff<T, U>はUにアサイン不可能な型のみをTから抽出した型である。
たとえば、以下のT1number | stringとなる。

type T2 = Diff<number | string | boolean, Boolean | object>
// T2 = number | string

Tに指定されたUnion typesの型(number | string | boolean)はそれぞれについてConditional typesとして判定されるため

T U T extends U ? 結果
number Boolean │ object false number
string Boolean │ object false string
boolean Boolean │ object true never

よって number | string になる。

null/undefinedを除外した型NonNullable

先ほど作成したDiff型を使う。

type NonNullable<T> = Diff<T, null | undefined>;

関数のプロパティ名をUnion typesとして成すFunctionPropertyName型

type FunctionPropertyName<T> = {
  [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

具体例からイメージを沸かせるためHumanインタフェースを定義し、それを交えて説明する。

interface Human {
  id: number;
  name: string;
  hello(): void;
  goodBye(): boolean;
}

keyof and Lookup Typesである[keyof T]を外して、まずはMapped Typesの解析をする。

{
  [K in keyof T]: T[K] extends Function ? K : never;
}

THumanを割り当て、Kを解くと以下の様になる。

{
  id: Human["id"] extends Function ? "id" : never;
  name: Human["name"] extends Function ? "name" : never;
  hello: Human["hello"] extends Function ? "hello" : never;
  goodBye: Human["goodBye"] extends Function ? "goodBye" : never;
}

よって

{
  id: never;
  name: never;
  hello:  "hello";
  goodBye:  "goodBye";
}

これに[key of T]をつけると..

{
  id: never;
  name: never;
  hello:  "hello";
  goodBye:  "goodBye";
}[keyof Human]

よって "hello" | "goodBye" になる。

Type inference in conditional types

HAD BETTER HARD

Conditional typesでマッチングしたパターンから、一部の型をinferでキャプチャできる。
具体例を見た方が分かりやすい。

Promiseで包まれた型を取り出すUnwrapPromise

type UnwrapPromise<T> = T extends Promise<infer U> ? U : any;

これは以下のように解釈できる。

  • UnwrapPromise<T>
    • TPromise<U> に割り当て可能なら(このケースではPromiseなら)、U
    • そうでなければany
  • infer UPromise<U>というパターンにマッチするUをキャプチャする

具体例。

type R = UnwrapPromise<Promise<number>>
// => number
type S = UnwrapPromise<number>
// => any (Promiseで包まれていないのでパターンにマッチしない)

関数のReturnする型を取得するMyReturnType

type MyReturnType<T> = T extends (...arg: any[]) => infer R ? R : any;

これは以下のように解釈できる。

  • MyReturnType<T>
    • T(...arg: any[]) => R に割り当て可能なら(このケースでは関数なら)、R
    • そうでなければany
  • infer R(...arg: any[]) => RというパターンにマッチするRをキャプチャする

具体例。

type R = MyRetrunType<Math.atan2>
// => number
type S = MyRetrunType<number>
// => any (関数ではないのでパターンにマッチしない)

co-variantの位置関係にある同一な複数型における推論

{ a: infer U, b: infer U }のような共変位置にある場合、推論結果はUnion typeになる。

interface A {
  p1: "x" | "y";
  p2: "y" | "z";
}

type Property<T> = T extends { p1: infer U, p2: infer U } ? U : never;
type N = Property<A>;
// => "y" | "z" | "x"

contra-variantの位置関係にある同一な複数型における推論

{ a: (x: infer U) => void, b: (x: infer U) => void }のような反変位置にある場合、推論結果はIntersection typeになる。

interface A {
  p1: (arg: "x" | "y") => void;
  p2: (arg: "y" | "z") => void;
}

type Property<T> = T extends { p1: (arg: infer U) => void, p2: (arg: infer U) => void } ? U : never;
type N = Property<A>;
// => "y"

Predefined conditional types

HAD BETTER EASY

Conditional typesを利用した新しい型が増えた。

説明
Exclude TからUに割り当て可能な型を除外する
Extract TからUに割り当て可能な型を抽出する
NonNullable Tからnull/undefinedの可能性を除外する
ReturnType 関数が返却する型
InstanceType コンストラクタが作成する型

具体例。

type Excluded = Exclude<"a" | "b" | "c", "b" | "c">
// "a"
type Extracted = Extract<"a" | "b" | "c", "b" | "c">
// "b" | "c"
type NonNullableString = NonNullable<string | null | undefined>
// string
type AbsReturnType = ReturnType<typeof Math.abs>
// number

class Human {
    constructor(public id: number, public name: string) { }
}
type HumanInstanceType = InstanceType<typeof Human>
// Human

Improved control over mapped type modifiers

HAD BETTER EASY

Mapped typesreadonly?が外せるようになった。

今までもできたこと

interface Human {
  readonly myNo: number;
  name: string;
  favorite?: string;
}

// すべてのプロパティをOptionalにする
type ExOptional<T> = { [P in keyof T]?: T[P] };
/*
🧑‍🎓 T = Humanの場合
interface ExOptional<Human> {
  readonly myNo?: number;
  name?: string;
  favorite?: string;
}
 */

// すべてのプロパティを変更不可にする
type ExReadonly<T> = { readonly [P in keyof T]: T[P] };
/*
🧑‍🎓 T = Humanの場合
interface ExReadonly<Human> {
  readonly myNo: number;
  readonly name: string;
  readonly favorite?: string;
}
 */

今まではできなかったこと

今回できるようになったこと。

  • readonly?の前に-を付けると削除になる
  • +を付けると追加だが省略しても同じ
interface Human {
  readonly myNo: number;
  name: string;
  favorite?: string;
}

// すべてのプロパティをRequiredにする
// ?を消すだけでなく、A | undefined => A のようにundefinedも消す
type ExRequired<T> = { [P in keyof T]-?: T[P] };
/*
🧑‍🎓 T = Humanの場合
interface ExRequired<Human> {
  readonly myNo: number;
  name: string;
  favorite: string;
}
 */

// すべてのプロパティを変更可能にする
type ExMutable<T> = { -readonly [P in keyof T]: T[P] };
/*
🧑‍🎓 T = Humanの場合
interface ExMutable<Human> {
  myNo: number;
  name: string;
  favorite?: string;
}
 */

Improved keyof with intersection types

HAD BETTER EASY

key ofにintersection typesを指定したとき、union typesに変換されるようになった。

具体的には以下のようなtype A, Bが存在するとき..

type A = { a: string };
type B = { b: number };

keyof (A & B)keyof A | keyof B と変換され "a" | "b" とみなされる。

Better handling for namespace patterns in .js files

UNKNOWN CAN NOT UNDERSTAND

JavaScriptファイル内で、トップレベルに宣言された空Objectはnamespaceと見なされるようになった。

var ns = {};   // namespaceと見なされる

どこで使うのか分からん。。

IIFEs as namespace declarations

UNKNOWN EASY

関数、クラス、空Objectを返す即時関数は名前空間として扱われる。

どこで使うのか分からん。。

Defaulted declarations

UNKNOWN CAN NOT UNDERSTAND

変更内容、メリット含めて全く分からない。。

Prototype assignment

NOT NECESSARY EASY

Objectリテラルを直接prototypeプロパティに代入できるようになった。

C.prototype.func = function() { ... };
C.prototype = {
  func() { ... };
};

Nested and merged declarations

UNKNOWN CAN NOT UNDERSTAND

変更内容、メリット含めて全く分からない。。

Per-file JSX factories

UNKNOWN EASY

ファイルの先頭に@jsx domプラグマを使用することで、ファイル毎にJSXファクトリを変更できる。

※ 普段JSXを使用しないので使い所は分からず..

Locally scoped JSX namespaces

UNKNOWN EASY

名前空間JSXがglobalではなくjsxNamespaceの下で検索されるようになった。
jsxNamespaceに何も定義されていない場合は、下位互換性を維持するためglobalのJSXが使われる。

New --emitDeclarationOnly

HAD BETTER EASY

tsc--emitDeclarationOnlyオプションをつけると、型ファイル(.d.ts)だけを生成する。
※ つまり、*.js*.jsxを生成しない

別途--declarationオプションの指定が必要。つけないとエラーになる。

transpileの手段にbabelなどを使いたいケースに使える。