Tackling TypeScript
この本のサポートをお願いします: 購入する または 寄付する
(広告です。ブロックしないでください。)

23 型の計算の概要



この章では、TypeScriptでコンパイル時に型を使って計算する方法を探ります。

この章では、型を使って計算する方法を学ぶことに重点を置いています。そのため、リテラル型を多用し、例は実用性は低いです。

23.1 メタ値としての型

TypeScriptコードの次の2つのレベルを考えてみましょう。

型レベルは、プログラムレベルのメタレベルです。

レベル 利用可能時期 オペランド 操作
プログラムレベル 実行時 関数
型レベル コンパイル時 特定の型 ジェネリック型

型で計算できるというのはどういう意味でしょうか?次のコードは例です。

type ObjectLiteralType = {
  first: 1,
  second: 2,
};

// %inferred-type: "first" | "second"
type Result = keyof ObjectLiteralType; // (A)

A行では、以下の手順を実行しています。

型レベルでは、以下の「値」で計算できます。

type ObjectLiteralType = {
  prop1: string,
  prop2: number,
};

interface InterfaceType {
  prop1: string;
  prop2: number;
}

type TupleType = [boolean, bigint];

//::::: Nullish types and literal types :::::
// Same syntax as values, but they are all types!

type UndefinedType = undefined;
type NullType = null;

type BooleanLiteralType = true;
type NumberLiteralType = 12.34;
type BigIntLiteralType = 1234n;
type StringLiteralType = 'abc';

23.2 ジェネリック型: 型のファクトリー

ジェネリック型は、メタレベルでの関数です。例えば、

type Wrap<T> = [T];

ジェネリック型Wrap<>にはパラメータTがあります。その結果は、タプル型でラップされたTです。このメタ関数を使用する方法は次のとおりです。

// %inferred-type: [string]
type Wrapped = Wrap<string>;

パラメータstringWrap<>に渡し、結果にエイリアスWrappedを付けます。結果は、単一のコンポーネント(型string)を持つタプル型です。

23.3 ユニオン型とインターセクション型

23.3.1 ユニオン型 (|)

型演算子|は、ユニオン型を作成するために使用されます。

type A = 'a' | 'b' | 'c';
type B = 'b' | 'c' | 'd';

// %inferred-type: "a" | "b" | "c" | "d"
type Union = A | B;

Aと型`B`を集合とみなすと、`A | B`はこれらの集合の集合論的和集合です。言い換えれば、結果のメンバーは、少なくとも1つのオペランドのメンバーです。

構文的には、ユニオン型の最初のコンポーネントの前に`|`を置くこともできます。これは、型の定義が複数行にまたがる場合に便利です。

type A =
  | 'a'
  | 'b'
  | 'c'
;
23.3.1.1 メタ値の集合としてのユニオン

TypeScriptは、メタ値の集合をリテラル型のユニオンとして表します。すでにその例を見てきました。

type Obj = {
  first: 1,
  second: 2,
};

// %inferred-type: "first" | "second"
type Result = keyof Obj;

すぐに、そのようなコレクションをループするための型レベルの操作が出てきます。

23.3.1.2 オブジェクト型のユニオン

ユニオン型の各メンバーは、少なくとも1つのコンポーネント型のメンバーであるため、すべてのコンポーネント型で共有されているプロパティにのみ安全にアクセスできます(A行)。他のプロパティにアクセスするには、型ガードが必要です(B行)。

type ObjectTypeA = {
  propA: bigint,
  sharedProp: string,
}
type ObjectTypeB = {
  propB: boolean,
  sharedProp: string,
}

type Union = ObjectTypeA | ObjectTypeB;

function func(arg: Union) {
  // string
  arg.sharedProp; // (A) OK
  // @ts-expect-error: Property 'propB' does not exist on type 'Union'.
  arg.propB; // error

  if ('propB' in arg) { // (B) type guard
    // ObjectTypeB
    arg;

    // boolean
    arg.propB;
  }
}

23.3.2 インターセクション型 (&)

型演算子&は、インターセクション型を作成するために使用されます。

type A = 'a' | 'b' | 'c';
type B = 'b' | 'c' | 'd';

// %inferred-type: "b" | "c"
type Intersection = A & B;

型`A`と型`B`を集合とみなすと、`A & B`はこれらの集合の集合論的共通部分です。言い換えれば、結果のメンバーは両方のオペランドのメンバーです。

23.3.2.1 オブジェクト型のインターセクション

2つのオブジェクト型のインターセクションは、両方の型のプロパティを持ちます。

type Obj1 = { prop1: boolean };
type Obj2 = { prop2: number };
type Both = {
  prop1: boolean,
  prop2: number,
};

// Type Obj1 & Obj2 is assignable to type Both
// %inferred-type: true
type IntersectionHasBothProperties = IsAssignableTo<Obj1 & Obj2, Both>;

(ジェネリック型IsAssignableTo<>については後述します。)

23.3.2.2 ミックスインにインターセクション型を使用する

オブジェクト型`Named`を別の型`Obj`にミックスインする場合、インターセクション型が必要です(A行)。

interface Named {
  name: string;
}
function addName<Obj extends object>(obj: Obj, name: string)
  : Obj & Named // (A)
{
  const namedObj = obj as (Obj & Named);
  namedObj.name = name;
  return namedObj;
}

const obj = {
  last: 'Doe',
};

// %inferred-type: { last: string; } & Named
const namedObj = addName(obj, 'Jane');

23.4 制御フロー

23.4.1 条件付き型

条件付き型は、次の構文を持ちます。

«Type2» extends «Type1» ? «ThenType» : «ElseType»

Type2が`Type1`に代入可能な場合、この型式の結果は`ThenType`です。そうでない場合は、`ElseType`です。

23.4.1.1 例: プロパティ`.length`を持つ型のみをラップする

次の例では、`Wrap<>`は、値が数値であるプロパティ`.length`を持っている場合にのみ、型を1要素のタプルにラップします。

type Wrap<T> = T extends { length: number } ? [T] : T;

// %inferred-type: [string]
type A = Wrap<string>;

// %inferred-type: RegExp
type B = Wrap<RegExp>;
23.4.1.2 例: 代入可能性の確認

条件付き型を使用して、代入可能性チェックを実装できます。

type IsAssignableTo<A, B> = A extends B ? true : false;

// Type `123` is assignable to type `number`
// %inferred-type: true
type Result1 = IsAssignableTo<123, number>;

// Type `number` is not assignable to type `123`
// %inferred-type: false
type Result2 = IsAssignableTo<number, 123>;

型の関係「代入可能性」の詳細については、[コンテンツは含まれていません]を参照してください。

23.4.1.3 条件付き型は分配的である

条件付き型は分配的です。条件付き型`C`をユニオン型`U`に適用することは、`C`を`U`の各コンポーネントに適用したユニオンと同じです。これは例です。

type Wrap<T> = T extends { length: number } ? [T] : T;

// %inferred-type: boolean | [string] | [number[]]
type C1 = Wrap<boolean | string | number[]>;

// Equivalent:
type C2 = Wrap<boolean> | Wrap<string> | Wrap<number[]>;

言い換えれば、分配性により、ユニオン型のコンポーネントを「ループ」することができます。

これは、分配性の別の例です。

type AlwaysWrap<T> = T extends any ? [T] : [T];

// %inferred-type: ["a"] | ["d"] | [{ a: 1; } & { b: 2; }]
type Result = AlwaysWrap<'a' | ({ a: 1 } & { b: 2 }) | 'd'>;
23.4.1.4 分配的条件付き型では、型`never`を使用して無視する

集合として解釈すると、型`never`は空です。したがって、ユニオン型に現れても無視されます。

// %inferred-type: "a" | "b"
type Result = 'a' | 'b' | never;

つまり、`never`を使用してユニオン型のコンポーネントを無視できます。

type DropNumbers<T> = T extends number ? never : T;

// %inferred-type: "a" | "b"
type Result1 = DropNumbers<1 | 'a' | 2 | 'b'>;

thenブランチとelseブランチの型式を入れ替えると、こうなります。

type KeepNumbers<T> = T extends number ? T : never;

// %inferred-type: 1 | 2
type Result2 = KeepNumbers<1 | 'a' | 2 | 'b'>;
23.4.1.5 組み込みユーティリティ型: Exclude<T, U>

ユニオンから型を除外することは非常に一般的な操作であるため、TypeScriptは組み込みユーティリティ型`Exclude<T, U>`を提供しています。

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;

// %inferred-type: "a" | "b"
type Result1 = Exclude<1 | 'a' | 2 | 'b', number>;

// %inferred-type: "a" | 2
type Result2 = Exclude<1 | 'a' | 2 | 'b', 1 | 'b' | 'c'>;
23.4.1.6 組み込みユーティリティ型: Extract<T, U>

`Exclude<T, U>`の逆は、`Extract<T, U>`です。

/**
 * Extract from T those types that are assignable to U
 */
type Extract<T, U> = T extends U ? T : never;

// %inferred-type: 1 | 2
type Result1 = Extract<1 | 'a' | 2 | 'b', number>;

// %inferred-type: 1 | "b"
type Result2 = Extract<1 | 'a' | 2 | 'b', 1 | 'b' | 'c'>;
23.4.1.7 条件付き型の連結

JavaScriptの三項演算子と同様に、TypeScriptの条件付き型演算子も連結できます。

type LiteralTypeName<T> =
  T extends undefined ? "undefined" :
  T extends null ? "null" :
  T extends boolean ? "boolean" :
  T extends number ? "number" :
  T extends bigint ? "bigint" :
  T extends string ? "string" :
  never;

// %inferred-type: "bigint"
type Result1 = LiteralTypeName<123n>;

// %inferred-type: "string" | "number" | "boolean"
type Result2 = LiteralTypeName<true | 1 | 'a'>;
23.4.1.8 inferと条件付き型

https://typescript.dokyumento.jp/docs/handbook/advanced-types.html#type-inference-in-conditional-types

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

type Syncify<Interf> = {
    [K in keyof Interf]:
        Interf[K] extends (...args: any[]) => Promise<infer Result>
        ? (...args: Parameters<Interf[K]>) => Result
        : Interf[K];
};

// Example:

interface AsyncInterface {
    compute(arg: number): Promise<boolean>;
    createString(): Promise<String>;
}

type SyncInterface = Syncify<AsyncInterface>;
    // type SyncInterface = {
    //     compute: (arg: number) => boolean;
    //     createString: () => String;
    // }

23.4.2 マップ型

マップ型は、キーのコレクションをループすることによってオブジェクトを生成します。例えば、

// %inferred-type: { a: number; b: number; c: number; }
type Result = {
  [K in 'a' | 'b' | 'c']: number
};

演算子`in`は、マップ型の重要な部分です。新しいオブジェクトリテラル型のキーがどこから来るかを指定します。

23.4.2.1 組み込みユーティリティ型: Pick<T, K>

次の組み込みユーティリティ型を使用すると、既存のオブジェクト型のどのプロパティを保持するかを指定することで、新しいオブジェクトを作成できます。

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

次のように使用されます。

type ObjectLiteralType = {
  eeny: 1,
  meeny: 2,
  miny: 3,
  moe: 4,
};

// %inferred-type: { eeny: 1; miny: 3; }
type Result = Pick<ObjectLiteralType, 'eeny' | 'miny'>;
23.4.2.2 組み込みユーティリティ型: Omit<T, K>

次の組み込みユーティリティ型を使用すると、既存のオブジェクト型のどのプロパティを省略するかを指定することで、新しいオブジェクト型を作成できます。

/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

説明

`Omit<>`は次のように使用されます。

type ObjectLiteralType = {
  eeny: 1,
  meeny: 2,
  miny: 3,
  moe: 4,
};

// %inferred-type: { meeny: 2; moe: 4; }
type Result = Omit<ObjectLiteralType, 'eeny' | 'miny'>;

23.5 その他の様々な演算子

23.5.1 インデックスタイプクエリ演算子 keyof

型演算子`keyof`にはすでに遭遇しました。これは、オブジェクト型のプロパティキーを一覧表示します。

type Obj = {
  0: 'a',
  1: 'b',
  prop0: 'c',
  prop1: 'd',
};

// %inferred-type: 0 | 1 | "prop0" | "prop1"
type Result = keyof Obj;

`keyof`をタプル型に適用すると、やや予期しない結果になる場合があります。

// number | "0" | "1" | "2" | "length" | "pop" | "push" | ···
type Result = keyof ['a', 'b', 'c'];

結果には以下が含まれます。

空のオブジェクトリテラル型のプロパティキーは、空集合`never`です。

// %inferred-type: never
type Result = keyof {};

`keyof`がインターセクション型とユニオン型をどのように処理するかは次のとおりです。

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

// %inferred-type: "a" | "b" | "shared"
type Result1 = keyof (A & B);

// %inferred-type: "shared"
type Result2 = keyof (A | B);

`A & B`が型`A`と型`B`の両方のプロパティを持っていることを覚えていれば、これは理にかなっています。`A`と`B`は共通のプロパティ`.shared`しか持っておらず、これが`Result2`を説明しています。

23.5.2 インデックスアクセス演算子 T[K]

インデックスアクセス演算子`T[K]`は、キーが型`K`に代入可能な`T`のすべてのプロパティの型を返します。`T[K]`は、ルックアップ型とも呼ばれます。

演算子が使用されている例を次に示します。

type Obj = {
  0: 'a',
  1: 'b',
  prop0: 'c',
  prop1: 'd',
};

// %inferred-type: "a" | "b"
type Result1 = Obj[0 | 1];

// %inferred-type: "c" | "d"
type Result2 = Obj['prop0' | 'prop1'];

// %inferred-type: "a" | "b" | "c" | "d"
type Result3 = Obj[keyof Obj];

角かっこ内の型は、すべてのプロパティキーの型(`keyof`によって計算される)に代入可能でなければなりません。そのため、`Obj[number]`と`Obj[string]`は許可されていません。ただし、インデックス付き型にインデックスシグネチャがある場合、`number`と`string`をインデックスタイプとして使用できます(A行)。

type Obj = {
  [key: string]: RegExp, // (A)
};

// %inferred-type: string | number
type KeysOfObj = keyof Obj;

// %inferred-type: RegExp
type ValuesOfObj = Obj[string];

`KeysOfObj`には型`number`が含まれています。これは、JavaScript(したがってTypeScript)では数値キーが文字列キーのサブセットであるためです。

タプル型もインデックスアクセスをサポートしています。

type Tuple = ['a', 'b', 'c', 'd'];

// %inferred-type:  "a" | "b"
type Elements = Tuple[0 | 1];

ブラケット演算子も分配的です。

type MyType = { prop: 1 } | { prop: 2 } | { prop: 3 };

// %inferred-type: 1 | 2 | 3
type Result1 = MyType['prop'];

// Equivalent:
type Result2 =
  | { prop: 1 }['prop']
  | { prop: 2 }['prop']
  | { prop: 3 }['prop']
;

23.5.3 タイプクエリ演算子 typeof

型演算子`typeof`は、(JavaScriptの)値をその(TypeScriptの)型に変換します。そのオペランドは、識別子またはドットで区切られた識別子のシーケンスでなければなりません。

const str = 'abc';

// %inferred-type: "abc"
type Result = typeof str;

最初の`'abc'`は値であり、2番目の`"abc"`はその型である文字列リテラル型です。

`typeof`を使用する別の例を次に示します。

const func = (x: number) => x + x;
// %inferred-type: (x: number) => number
type Result = typeof func;

§14.1.2「型にシンボルを追加する」では、`typeof`の興味深いユースケースについて説明しています。