個人的Typescriptの型入門③ 〜 Typescriptの型のディープな世界


※ 当ページには【広告/PR】を含む場合があります。
2024/09/10
2025/07/15
個人的Typescriptの型入門② 〜 オブジェクト型の応用
TypesriptでもBitwiseなテンプレートリテラル型で簡単ビット演算チェック
蛸壺の技術ブログ|Typescriptの型のディープな世界



前回はTypescriptのオブジェクト型に応用的な使いこなしを中心にまとめてみました。

合同会社タコスキングダム|蛸壺の技術ブログ
個人的Typescriptの型入門② 〜 オブジェクト型の応用

個人的「型」入門〜オブジェクト型をもっと掘り下げる



今回はTypescriptの応用をフル活用した「型プログラミング」のディープな世界に足を踏み入れる内容になります。
前回の記事同様、以下の詳しい解説されているサイトをベースに、著者が防備録として抑えておきたい利用法をピックアップしています。

参考|TypeScriptの型入門

詳しい説明はそちらのほうをご参照ください。


合同会社タコスキングダム|蛸壺の技術ブログJavascript(js)&Typescript(ts)プログラミング入門〜これから学ぶ人のためのおすすめ書籍&教材の手引き

型の学習には「Typescript Playground」を使おう



復習も兼ねて、型の基礎のほうから徐々にTypescriptの型の使い方を慣らして行きましょう。
なおTypescriptの型チェックは、VSCode等のエディタで用意されているものを扱うことを想定していますが、サッとTypescriptを試したい際には定番の
「Typescript Playground」 を使うのがもっとも手っ取り早いでしょう。

Typescript Playground
合同会社タコスキングダム|蛸壺の技術ブログJavascript(js)&Typescript(ts)プログラミング入門〜これから学ぶ人のためのおすすめ書籍&教材の手引き

Typescriptによる「型プログラミング」



実際には、前回の内容のレベルでも十分Typescriptで動くアプリを実装していけるとは思いますが、既にTypeScript界隈では、少ないコードで高度な型を表現する「型芸」(「シェル芸」っぽく)の世界が花開いています。
一見何をやっているか良く分からないものの、そこに秘められた機能美にはただただ驚かされるような「作品」がオンラインでいくつも公開されています。

参考|TypeScript Awesome Template Literal Types

これらの"型芸"を理解するには、いくつか高度な構文に慣れる必要があります。
ということで以降では、コツコツと小出しにテクニックをまとめていきます。


合同会社タコスキングダム|蛸壺の技術ブログJavascript(js)&Typescript(ts)プログラミング入門〜これから学ぶ人のためのおすすめ書籍&教材の手引き

keyof演算子

keyof演算子とは



とあるオブジェクト型・
T に、 keyof T とすると、「Tのプロパティ名(キー)を全て持った型」になります。
ここではこのような型を
「keyof型」 と呼ぶことにします。
具体例で見てみましょう。

            interface MyObj {
    foo: string;
    bar: number;
}

//keyは'foo' | 'bar'型
let key: keyof MyObj;

key = 'foo';
key = 'bar';
//✘ Type '"baz"' is not assignable to type '"foo" | "bar"'.
key = 'baz';

        

この例では、
MyObj 型は、プロパティfooとbarを持ち、プロパティ名として可能な文字列は'foo'と'bar'のみであり、 keyof MyObj はそれらの文字列のみを受け付けるUnion型、すなわち 'foo' | 'bar' 型になります。
よって、
keyof MyObj の型にない'baz'というプロパティ名を代入しようとすると、エラーとなります。


symbol型をプロパティに含むオブジェクトへのkeyof



JavaScriptではオブジェクトのプロパティ名が、文字列以外の
「シンボル」 の場合があります。
ここではシンボルの使い方には興味がないので特には解説はしませんが、場合によっては、シンボルを使ったプロパティを持つオブジェクトにも
keyof したい場合があるかもしれません。


            //新しいシンボルを作成
const symb = Symbol();

const obj = {
    foo: 'str',
    [symb]: 'symb',
};

//'foo' | typeof symb 型
type ObjType = keyof (typeof obj);

        

この例では、objの値から
typeof で型推論されるオブジェクト型に keyof することで、オブジェクトのプロパティ名のUnion型を作っています。
結果的に、ここでの
ObjType 型は 'foo' | typeof symb 型となっています。
注目すべきは、オブジェクトのプロパティにシンボルの型を含む場合、そのオブジェクト型を
keyof すると、 typeof <シンボル名> 型に推論されるようです。
TypeScriptではシンボルはsymbol型ですが、プロパティ名としてはシンボルはひとつずつ実体が異なるため
symb に入っている特定のシンボルでないといけないのです。

'foo' | symbol とはならない点に注意してください。

keyofの注意点



数値リテラルを使ってプロパティを宣言した場合、keyof型にはnumberの部分型が含まれる場合もあります。

            const obj = {
    foo: 'str',
    0: 'num',
};

//0 | 'foo'型
type ObjType = keyof (typeof obj);

        

なお、TypeScriptでは数値型をプロパティのキーに指定できますが、JavaScriptにはプロパティキーに数値は使えないルールのため、文字列へ強制的に変換されます。

インデックスシグネチャとkeyof演算子



前回説明した「インデックスシグネチャ」を持つオブジェクト型に、
keyof した場合、特殊な挙動になります。

            interface MyObj {
    [foo: string]: number;
}

//MyObjKeyは、string | number型
type MyObjKey = keyof MyObj;

        

この例で、
MyObj 型は、任意のstring型のプロパティ名をとることができるため、 keyof MyObj はstring型になると予想は付きます。
しかし実際には、
string | number となっているのは、Javascriptコード変換後は数値のキーもどうせ文字列扱いになるだろうという意図があるようです。
一方で、以下のコードで、インデックスシグネチャのキーがnumber型の場合、
keyof MyObj はnumber型のみになることが分かります。

            interface MyObj {
    [foo: number]: string;
}

//MyObjKeyは、number型
type MyObjKey = keyof MyObj;

        

合同会社タコスキングダム|蛸壺の技術ブログJavascript(js)&Typescript(ts)プログラミング入門〜これから学ぶ人のためのおすすめ書籍&教材の手引き

Lookup型

Lookup型とは



「Lookup型」は、2つの型変数
TK に対して、 T[K] という構文で表される型です。

K はプロパティ名の型で、なんでも良いというわけではなく、 string | number | symbol (別名・ PropertyKey )を満たす必要があります。
このような条件下で、
T[K] がK型のキーの値の型となる、ということを表現しています。
以下の例で説明すると、

            interface MyObj {
    foo: string;
    bar: number;
}

//strはstring型
const str: MyObj['foo'] = '123';

        

この例での、
MyObj['foo'] という型が、ここでの「Lookup型」です。

T[K] の対応では、Tが MyObj 型、Kが 'foo' 型となります。
よって、
MyObj['foo'] 型は、「MyObj型のfooというプロパティの型」、すなわち string 型となります。
同様の考え方で、
MyObj['bar'] なら、 number 型です。
なお、
MyObj['baz'] のようにプロパティ名にはない型を与えるとエラーとなります。
つまり、Lookup型の型変数
K は、 keyof T 型の部分型となっていることが以下の例でも分かります。

            interface MyObj {
    foo: string;
    bar: number;
}

//strはstring | numbder型
const str: MyObj[keyof MyObj] = 123;

        

実用性は少ないですが
MyObj[keyof MyObj] という型はもちろん可能で、ここでは MyObj['foo' | 'bar'] と同じで、結果は string | number 型になっています。

keyof型とLookup型を組み合わせた利用法



先程の説明の通り、Lookup型(
T[K] )の型変数 K は、 keyof T 型の部分型とみなせるが故に、keyof型とLookup型は良くセットとなって利用されます。
以下の例を見てみましょう。

            function pick<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const obj = {
    foo: 'string',
    bar: 123,
};

const str: string = pick(obj, 'foo');
const num: number = pick(obj, 'bar');

//✘ Argument of type '"baz"' is not assignable to parameter of type '"foo" | "bar"'.
pick(obj, 'baz');

        

この関数
pick は、 pick(obj, 'foo') のように使うと、 obj.foo の値を返してくれるような関数です。
注目すべきは、この関数が返す値に、正しい型を付けることができているという点です。

pick は2つの型変数 TK を持ち、特に KK extends keyof T と書かれています。
ここでの
K extends keyof T は、「宣言する型変数 K は、 keyof T の部分型でなければならない」の意味です。
この条件が無いと、返り値の型
T[K] が妥当でない可能性が生じるためエラーとなります。
pick(obj, 'foo')という呼び出しで、Tが
{ foo: string; bar: number; } 型、Kが 'foo' 型となり、その返り値の型は、 ({ foo: string; bar: number; })['foo'] 型 = string型となります。


合同会社タコスキングダム|蛸壺の技術ブログJavascript(js)&Typescript(ts)プログラミング入門〜これから学ぶ人のためのおすすめ書籍&教材の手引き

Mapped型

Mapped型とは



「Mapped型」は、
{[P in K]: T} として、 P という型変数と、 KT という何らかの型で表現される特殊な型です。
ただし
K に関しては、string型の部分型である必要があります。

{[P in K]: T} の意味は、 「K型の値として可能な各文字列Pに対して、型Tを持つプロパティPが存在するようなオブジェクト型」 です。
抽象的な定義では分かりにくいですが例えば、
{[P in 'foo' | 'bar']: number} というオブジェクト型は、Kにあたる部分は 'foo' | 'bar' で、Pは 'foo''bar' という文字列の値が可能であり、これらの名前のプロパティfooとbarがnumber型を持つようなオブジェクト型です。
すなわち、
{[P in 'foo' | 'bar']: number} 型は、 { foo: number; bar: number; } 型と同じ意味になることが以下の例でも確かめられます。

            type Obj1 = {[P in 'foo' | 'bar']: number};
interface Obj2 {
    foo: number;
    bar: number;
}

const obj1: Obj1 = {foo: 3, bar: 5};
const obj2: Obj2 = obj1;
const obj3: Obj1 = obj2;

        

Mapped型の特殊型〜「ホモモーフィックMapped型」



Mapped型の基本形・
{[P in K]: T} から、 {[P in keyof T]: U} のように表現を拡張したものは 「ホモモーフィックMapped型」 と呼ばれます。
"ホモモーフィック"は
準同型 という意味で、そもそもは代数的構造を持つ集合(代数系)で、fが準同型写像であれば、このfのみで、Aという集合から、別のBという集合へ、一対一に対応させることができることを指しています。
数学的な表現に言い換えると、写像が準同型であれば、「代数的構造を保存する」、「代数的構造と両立する」、「代数的構造と可換である」などと呼ぶようです。

参考|準同型

そんな"ホモモーフィック"という言葉のもつ、「構造を保存する」という意味合いにあやかって付けられたのが、「ホモモーフィックMapped型」です。
改めて
{[P in keyof T]: U} 型を見ると、作成元となるオブジェクト型 T と全く同じプロパティキーをもつため、オブジェクト型 T の「構造が保存されている」ことを表しているようです。
このことを踏まえて、単なるMapped型とホモモーフィックMapped型の違いを以下の例で見てみましょう。

            type Obj = {
  readonly num: number
  obj?: {
    str: string,
    readonly num: number 
  }
}

type keyForObj = "num" | "obj";

/**
 * 単なるMapped型(ホモモーフィックなマッピングではない)では構造は保存されない
 * type NonHomomorphic = {
 *   num: number;
 *   obj: {
 *     str: string;
 *     readonly num: number;
 *    } | undefined;
 * }
 */
type NonHomomorphic = {
  [P in keyForObj]: Obj[P]
}

/**
 * ホモモーフィックなマッピングで構造は保存される
 * type Homomorphic = {
 *   readonly num: number;
 *   obj?: {
 *     str: string;
 *     readonly num: number;
 *   } | undefined;
 * }
 */
type Homomorphic = {
  [P in keyof Obj]: Obj[P]
}

        

このホモモーフィックMapped型の利用の幅広く、Mapped型のテクニックのほとんどこのホモモーフィックMapped型になっているのではないでしょうか。
例えば次のような例で考えてみましょう。

            type PropNullable<T> = {[P in keyof T]: T[P] | null};

interface Foo {
    foo: string;
    bar: number;
}

const obj: PropNullable<Foo> = {
    foo: 'foobar',
    bar: null,
};

        

ここでは型変数
T を持つ型・ PropNullable<T> を定義しています。
この型は、T型のオブジェクトの各プロパティPの型が、
T[P] | null 、すなわち元の型かnullかのいずれかであるようなオブジェクト型です。
この例では、
PropNullable<Foo> 型は、 {foo: string | null; bar: number | null; } 型と同じになり、構造が保存ことが分かります。

Mapped型とプロパティ修飾子



Mapped型の
[P in K] 部に、プロパティ修飾子(?かreadonly)を付けることもできます。

            type Partial<T> = {[P in keyof T]?: T[P]};

        

TypeScriptの標準ライブラリにもある
Partial<T> 型は、上のような定義を持ち、Tのプロパティを全てオプショナルにした型となります。
全てのプロパティをreadonlyにする
Readonly<T> も標準ライブラリとしてはありますが、実装するならば以下のようになるでしょう。

            type Readonly<T> = {readonly [P in keyof T]: T[P]};

        

Mapped型で修飾子を取り除く



逆に、付与されたプロパティ修飾子を取り除くことです。
そのためには、?やreadonlyの前に
「-」 を付けます。

            type MyRequired<T> = {[P in keyof T]-?: T[P]};

        

この例では、T型のすべてのプロパティから
「?」を取り除いた (つまり標準では Required<T> )型になります。

            type MyRequired<T> = {[P in keyof T]-?: T[P]};

interface Foo {
    foo: string;
    bar?: number;
}

///ReqFooは{ foo: string; bar: number; }型
type ReqFoo = MyRequired<Foo>;

        

使用例してみると、
ReqFoo 型から、barプロパティが「?」が無くなっていることが分かります。

Mapped型のさらなる応用



ライブラリの型定義ファイルなどを眺めると、Mapped型を使った関数にしばしば遭遇します。
例えば以下は、オブジェクトリテラルから推論された型のプロパティを全てstring型した型を返す

            function propStringify<T>(obj: T): {[P in keyof T]: string} {
    const result = {} as {[P in keyof T]: string};
    // const result = {} as any;でも可
    for (const key in obj) {
        result[key] = String(obj[key]);
    }
    return result;
}

const foo = {foo: 'foo', bar: 123};

//aは{foo: string; bar: string; }型
const a = propStringify(foo);

        

この例で、asを使ってresultの型を
{[P in keyof T]: string} にしてから、実際にひとつずつプロパティを処理して追加します。

Mapped型と引数の位置



Mapped型を関数の引数でも書くことはできます。

            function pickFirst<T>(obj: {[P in keyof T]: Array<T[P]>}): {[P in keyof T]: T[P] | undefined} {
    const result: any = {};
    for (const key in obj) {
        result[key] = obj[key][0];
    }
    return result;
}

const obj = {
    foo: [0, 1, 2],
    bar: ['foo', 'bar'],
    baz: [],
};

const picked = pickFirst(obj);
picked.foo; // number | undefined型
picked.bar; // string | undefined型
picked.baz; // undefined型

        

この例ではまず、変数objは
{ foo: number[]; bar: string[]; baz: never[]; } 型であり、それが {[P in keyof T]: Array<T[P]>} の部分型であることを用いて、型Tを { foo: number; bar: string; baz: never; } 型だと推論できています。
これからさらにMapped型でから、返り値の型を
{ foo: number | undefined; bar: string | undefined; baz: undefined; } 型にしています。
なお、bazの型はUnion型の
never | undefined ですが、 never型の性質 から undefined 型に集約されます。
ここで注目すべきは、
pickFirst 関数の型引数TをTypescript側にその定義を教えてあげなくても、正しく推論できている点にあります。


合同会社タコスキングダム|蛸壺の技術ブログJavascript(js)&Typescript(ts)プログラミング入門〜これから学ぶ人のためのおすすめ書籍&教材の手引き

Conditional型

Conditional型とは

「conditional型」 は、4つの型変数を用いて、 T extends U ? X : Y という構文で型の条件分岐をすることで引き出せる結果の型を自由に構成することのできる型です。
この例でいうと、通常の条件三項演算子のように、この型は
TU の部分型ならば X 、そうでなければ Y 、という意味になります。
例えば以下のような実装が考えられます。

            type A<T> = T extends string ? string : number;

//Bはstring型
type B = A<string>;

//Cはnumber型
type C = A<boolean>;

//Dはstring型
type D = A<'hello'>;

//Eはnumber型
type E = A<123>;

        

復習になりますが、もともとTypescriptにおける
「extends演算子」 はinterfaceの継承・拡張だったものが、「Conditional型」の登場により、別の重要な役割を与えられました。
Conditional型で用いられる
「T extends U」 は、 「TがUの部分型ならばtrue、そうでないならfalse」 を返す演算子になります。
当然プリミティブ型に限らず、型変数にオブジェクト型を指定しても期待どおりに処理されます。

            type UserA = {
    name: string;
    age: number;
};

type UserB = {
    name: string;
    email: string;
};

type UserC = {
    name: string;
    age: number;
    email: string;
};

type U<T> = T extends UserA ? number : string;

//Aはnumber型
type A = U<UserA>;

//Bはstring型
type B = U<UserB>;

//Cはnumber型
type C = U<UserC>;

        

Mapped型の限界とConditional型



他の言語でもしばしば問題になりますが、オブジェクトのインスタンスを複製する際に、そのコピーが「深い(deep)」か「浅い(shallow)」か、がテーマに挙がります。
実は「Mapped型」でのマッピングは、shallowであり、単体だとdeepな結果は得られません。
例えば上の節で取り上げていた
Readonly<T> などは、プロパティをshallowにreadonlyへ変換するだけです。


            type MyReadonly<T> = {readonly [P in keyof T]: T[P]};

interface Obj{
    foo: string;
    bar: {
        hoge: number;
    };
}

//Aは以下の型:
//{
//  readonly foo: string;
//  readonly bar: {
//    hoge: number;
//  };
//}
type A = MyReadonly<Obj>;

const a: A = {
  foo: 'aaa',
  bar: {
    hoge: 123
  }
}

//Cannot assign to 'foo' because it is a read-only property.
a.foo = 'bbb';

//readonlyではないので書き換えOK
a.bar.hoge = 456;

        

例えばこの場合、
Obj 型に対して、 MyReadonly<Obj>{readonly foo: string; readonly bar: { hoge: number; };} 型となっています。
注意すべきはこの型の中で、barのプロパティhogeがreadonlyになっていないので、deepなマッピングにはなっていないのです。
ではどうすればdeepなMapped型にできるのかというと、以下のような再帰的な定義にしてみます。

            type DeepReadonly<T> = {
    readonly [P in keyof T]: DeepReadonly<T[P]>;
}

        

これを実際に試してみますと、

            type DeepReadonly<T> = {
    readonly [P in keyof T]: DeepReadonly<T[P]>;
}

interface Obj{
    foo: string;
    bar: {
        hoge: number;
    };
}

//Aは以下の型:
//{
//  readonly foo: string;
//  readonly bar: DeepReadonly<{
//    hoge: number;
//  }>;
//}
type A = DeepReadonly<Obj>;

const obj: A = {
    foo: 'foo',
    bar: {
        hoge: 3,
    },
};

//✘ Cannot assign to 'hoge' because it is a constant or a read-only property.
obj.bar.hoge = 3;

        

となり、deepなreadonlyなプロパティになっています。
最近のTypescriptではより推論能力が強化され、
DeepReadonly<T> の型Tが十分判明していなくても、以下のように再帰的なマッピング操作が可能です。

            type DeepReadonly<T> = {
    readonly [P in keyof T]: DeepReadonly<T[P]>;
}

function readonlyify<T>(obj: T): DeepReadonly<T> {
    return obj as DeepReadonly<T>;
}

const obj = {
    foo: 'foo',
    bar: {
        hoge: 3,
        piyo: {
            moge: 8
        }
    },
};

const a = readonlyify(obj);

//✘ Cannot assign to 'hoge' because it is a read-only property.
a.bar.hoge = 3;
//✘ Cannot assign to 'moge' because it is a read-only property.
a.bar.piyo.moge = 8;

        

Mapped型とConditional型を組み合わせた型操作



先程は再帰的なマッピング操作をMapped型単体で行う例を示しましたが、Conditional型を組み合わせることで、より汎用性の高い型表現ができるようになります。
例えば、
DeepReadonly<T> を以下のように定義し直してみましょう。

            type DeepReadonly<T> =
    T extends any[] ? DeepReadonlyArray<T[number]> :
    T extends object ? DeepReadonlyObject<T> :
    T;

interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {};

type DeepReadonlyObject<T> = {
    readonly [P in NonFunctionPropertyNames<T>]: DeepReadonly<T[P]>;
};

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

        

こちらはいくつかの型定義で構成される形になっています。
まず、本体の
DeepReadonly<T> は、Conditional型で構成され、最初の T が配列の場合、次に配列以外のオブジェクトの、そして最後にそれ以外(すなわちプリミティブ型)の順に評価されます。
配列型に型マッチした場合、
DeepReadonlyArray<T> による処理、それ以外のオブジェクト型は DeepReadonlyObject<T> で処理を分離しています。
プリミティブ型は、そのプロパティの型を考える必要はないため
T の型をそのまま返します。
細かく見ていくと、配列型をreadonlyにする
DeepReadonlyArray<T> は、要素の型である TDeepReadonly<T> で再帰的に処理し、配列自体の型は標準ライブラリの ReadonlyArray<T> により再表現します。
なお、
ReadonlyArray<T> は、各要素がreadonlyになっている配列を返します。
配列型
T の要素の型については、例えば T[number] と書くと、「Tに対してnumber型のプロパティ名でアクセスできるプロパティの型」と解釈されます。
次に、
DeepReadonlyObject<T> では、素朴にMapped型単体で各プロパティを処理しています。
ただし、
NonFunctionPropertyNames<T> というConditional型を利用した型定義を使って、 T のプロパティ名のうちで関数型のものを除外しています。

DeepReadonlyObject<T> で、 T からメソッド(関数型のプロパティ)を除去したい理由は、メソッドから自己を書き換える危険な操作を排除したいパターンを想定しているからです。

Conditional型は遅延評価される



先程の
DeepReadonly<T> の考え方のキモは、 「Conditional型は遅延評価される」 ということです。
(※現在ではMapped型も型変数の遅延評価しているようです。)
Conditional型の分岐条件では、型変数がそもそも何がなのかわからないと判定できないので、必然的に遅延評価となります。

            type List<T> = {
  value: T;
  next: List<T>;
} | undefined;

type A = DeepReadonly<List<number>>;

const a: A = {
    value: 1,
    next: {
        value: 2,
        next: undefined
    }
};

//Cannot assign to 'value' because it is a read-only property.
a.value = 2;
//Cannot assign to 'next' because it is a read-only property.
a.next!.next = {value: 3};

        

遅延評価により、型評価時に無限ループすることを防いでくれるため、
List<T> のような再帰的な型定義をもつ複雑な型でも、 DeepReadonly<T> を適用することが可能になります。


合同会社タコスキングダム|蛸壺の技術ブログJavascript(js)&Typescript(ts)プログラミング入門〜これから学ぶ人のためのおすすめ書籍&教材の手引き

infer修飾子

Conditional型における型抽出〜「infer」



Conditional型の強力な機能の一つである、
「infer」 修飾子を使うことでマッチした型を抽出することができます。
例えば、以下のような例です。

            type UserA = {
    name: string;
    role: 'admin' | 'user'; //この型を抽出したい
};

type UserB = {
    name: string;
    age:number
};

type A<T> = T extends { role: infer U } ? U : null;

type B = A<UserA>; //"admin" | "user"
type C = A<UserB>; // null

        

この場合、
T extends { key: infer U } ? U : V と型マッチパターンを作ることで、その型の内部のプロパティから推論される型を抽出してくれます。
型変数
T のroleプロパティが存在するかを評価し、存在する場合はその型を、存在しない場合はnull型を返えす、という処理になっています。
別の例で、標準ライブラリにもある
ReturnType<T> をより簡素化してみた MyReturnType<T> で、inferを使ってみます。

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

//Bはnumber型
type B = MyReturnType<number>;


//Cはvoid型
type C = MyReturnType<(arg: number[])=>void>;

        

この例では、型変数
T が、 (...args: any[])=>R 型の部分型であるとき R で評価され、そうでない場合にはany型が返ります。
なお標準ライブラリの
ReturnType<T> はほんの少し異なります。

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

        

inferの重ね掛け



同じ型変数に対して、inferを複数箇所に使うことも可能です。
inferを複数使う場合、推論される型がUnion型やIntersection型になるなどより複雑化します。
これは次の例で確かめられます。

            type Foo<T> = T extends {
    foo: infer U;
    bar: infer U;
    hoge: (arg: infer V)=> void;
    piyo: (arg: infer V)=> void;
} ? [U, V] : never;

interface Obj {
    foo: string;
    bar: number;
    hoge: (arg: string)=>void;
    piyo: (arg: number)=>void;
}

//tは[string | number, never]型
declare let t: Foo<Obj>;

        

型システムを考えれば、Uは共変な位置に、Vは反変な位置に出現しているため、UがUnion型で表現されて、VがIntersection型で表現されます。

Conditional型とinferによる文字列マッチング



最後に、inferとテンプレートリテラル型を組み合わせることで、型レベルの文字列操作が可能になります。
以下の例で試してみましょう。

            type PartialHello<S extends string> = S extends `Hello, ${infer P}!` ? P : unknown;

//"world"型
type T1 = PartialHello<"Hello, world!">;
//unknown型
type T2 = PartialHello<"Hell, world!">;

        

例えば、
"Hello, world!" 型という文字列リテラル型から、"Hello, "以外の部分を抜き出して、新しい文字列リテラル型にできるか否かまで判定してくれます。


合同会社タコスキングダム|蛸壺の技術ブログJavascript(js)&Typescript(ts)プログラミング入門〜これから学ぶ人のためのおすすめ書籍&教材の手引き

まとめ



当初自分のTypescriptテクニックの防備録集とするつもりが、結構な長丁場な内容になってしましました。
今後もさらなる進化を遂げるであろうTypescriptの型については、継続的に勉強していく必要がありますが、記事としては一旦ここで打ち止めです。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。

合同会社タコスキングダム|蛸壺の技術ブログJavascript(js)&Typescript(ts)プログラミング入門〜これから学ぶ人のためのおすすめ書籍&教材の手引き