Edited at

TypeScriptにおける型計算の基本

More than 1 year has passed since last update.


概要

TypeScript(以下TS)の型定義の最大の特徴は型を計算して指定できることだと思います。

この記事では、型計算を行うときの基本的な考え方を状況別にまとめました。

※特に複雑な型計算は、もっと簡単な記述方法があるかもしれませんが、ご了承ください


対象の環境

TypeScript 3.0.1


どのような人向きか?


  • 基本的な文法は理解されている。

  • やりたい定義は何となく浮かんでいるが、上手く書き起こせていない方。

  • 公式のドキュメント読んだが、具体的な使い方が分からなかった方。


状況別の基本的やり方


複数の型を受け入れたい

|を使うことで、型1または型2のいずれかを表現できます。


type InputTypes = 'text'|'textarea';
type NullableTypes = InputTypes|null;
// => 'text'|'textarea'|null;

型を計算する際は、例のようにnullundefinedを足すケースが多いと思います。


型を除きたい

T extends (除きたい型) ? never : Tで、型を除くことができます。


type NullableTypes = 'text'|'textarea'|null;
type NonNullableTypes<T> = T extends null ? never : T;
type InputTypes = NonNullableTypes<NullableTypes>;
// => 'text'|'textarea'|neverだが、neverが除かれるため'text'|'textarea'になる

補足すると、NonNullableTypes<T>Tとして渡された、|で区切られた全ての型に対し、nullであるか判定し、nullだった場合はneverを、そうでない場合元の型を返します。

neverは他の型が存在しているとき結果に残らないので、最終的な結果としてnullが除外されたものが取得できます。

オブジェクトのプロパティ全てから指定の型を取り除くのは、少し難しいので後述します。


別のプロパティを足したい

&を使うことで、プロパティを追加した新しい型を取得できます


interface TextParams {
type: 'text';
placeholder?: string;
}

type TextWithWidthParams = TextParams & {width?: number};
// => {type: 'text'; placeholder?: string; width?:number;}

ただし、既に同名のプロパティが存在している場合、どちらの型も満たす必要がありますので注意してください。


interface TextParams {
type: 'text';
placeholder?: string;
}

type TextDoublePlaceholderParams = TextParams & {placeholder?: number};
// => {type: 'text'; placeholder?: string&number; }

上の例の場合、string&numberとなる型は存在しないので、困ったことになります。


部分型を作りたい

{[P in 'プロパティ1'|'プロパティ2'...]: T[P]}で、ある型の部分型を作ることが出来ます。


interface TextWithWidthParams {
type: 'text';
placeholder?: string;
width?: number;
}

type TextParams = {[P in 'type'|'placeholder']: TextWithWidthParams[P];};
// => {type: 'text'; placeholder: string;}

ただしこの書き方では通常、型とは異なる?(省略可能)readonlyは引き継げません。

省略可能と不可能が混在していて、その属性を引き継ぎたい場合後述する型計算を行う必要があります。

※ 執筆した際のバージョンでは、未加工になることが保証されるケースのみ引き継ぎます。最適化の都合でしょうか。


interface TextWithWidthParams {
type: 'text';
placeholder?: string;
width?: number;
}
type Copy<T> = {[P in keyof T]: T[P];};
type CopiedTextWithWidthParams = Copy<TextWithWidthParams>;
// => {type: 'text'; placeholder?: string; width?: number;}
type CopiedTextWithWidthParams2 = {[P in 'type'|'placeholder'|'width']: TextWithWidthParams[P];};
// => {type: 'text'; placeholder: string; width: number;}


応用編

基本的な文法で紹介した4つの構文を知っていれば、大半の型計算は実装できます。

ここでは、実際に計算した例を紹介します。


ある型のプロパティ全てからnullを取り除きたい


interface NullableTextParams {
type: 'text';
placeholder?: string|null;
width?: number|null;
}
type ConvertNonNullable<T> = {[P in keyof T]: T[P] extends null ? never : T[P];};
type TextParams = ConvertNonNullable<NullableTextParams>;
// => {type: 'text'; placeholder: string; width: number;}

部分型を作りたい型を除きたいの組み合わせです。

keyofTのプロパティ名を全て取得できますので、順番にnullと比較し、除いていきます。


別にデフォルトが指定されているプロパティは省略可能にしたい

部分型を作りたい別のプロパティを足したいの組み合わせです。

省略不可能は省略可能よりも優先されるため、デフォルトで指定されたものと指定されていないものに分けて足し合わせます。


// 組み込みのExcludeでDefaultに存在しているプロパティ名を除外し、それを元に部分型を作る
type RequiredParams<T, Default> = { [P in Exclude<keyof T, keyof Default>]: T[P] };
// ExtractはExcludeの逆
type OptionalParams<T, Default> = { [P in Extract<keyof T, keyof Default>]?: T[P] };
type ConsiderDefault<T, Default> = RequiredParams<T, Default> & OptionalParams<T, Default>;

interface UserParams {
id: number;
name: string;
sex: string;
address: string;
phone: string;
zipCode: string;
mail: string;
isDeleted: boolean;
}

class User {
private _params: UserParams;

constructor(params: ConsiderDefault<UserParams, ReturnType<User['default']>>) {
this._params = { ...this.default(), ...params };
}

private default() {
return {
address: '',
phone: '',
zipCode: '',
isDeleted: false,
};
}
}
new User({id: 1, name: '田中太郎', sex: 'man', mail:'taro.tanaka@example.com', address: '東京'});
// => defaultで指定されたものは省略可能になる


特定の型のプロパティのみからなる部分型を作りたい

よくあるケースだと思いますが、やけに難しかったりします。

こういったケースの基本的なポイントは、対応するプロパティ名をいかにして取得するかです

とりあえず以下のようなデータを想定し、stringのプロパティだけ取得する計算を考えてみます。


interface UserParams {
id: number;
name: string;
sex: string;
address: string;
phone: string;
zipCode: string;
mail: string;
isDeleted: boolean;
}

プロパティ名を取得するとき、一番最初に思いついたのは以下のような計算でした。


// NGな例
type ExtProps<T, P extends keyof T, Type> = T[P] extends Type ? P : never;
type StrProps = ExtProps<UserParams, 'id'|'name'|'sex', string>;
// => never

上の方法は何がダメなのでしょうか?答えはT[P]にあります。

Pは複数のプロパティ名が混ざっているため、stringでないプロパティが1つでも含まれているとneverになってしまいます。

解消するためには、ExtPropsに渡す際にプロパティを個別にしなければなりません。


type ExtProps<T, P extends keyof T, Type> = T[P] extends Type ? P : never;
type StrProps = ExtProps<UserParams, 'id', string> | ExtProps<UserParams, 'name', string> | ExtProps<UserParams, 'sex', string>...;
// => 'name'|'sex'|'address'|'phone'|'zipCode'|'mail'

毎回手で列挙するのは面倒なので、自動で計算されるようにします。

(ついでに、省略可能に対応するためのロジックも足しています。)

type TypeFilter<T, Type> = T extends Type ? never : T;

// 指定のプロパティにある型が含まれているかどうかは、その型を取り除いた結果と一致するかで判定できる
type ExtProps<T, P extends keyof T, Type> = T[P] extends TypeFilter<T[P], Type> ? never : P;

// まず、{id: never; name: 'name'; sex: 'sex'...} の形に変換される
// 続いて[keyof T]で全ての値の型が|で繋がるため、never|'name'|'sex'...の形式になり、neverが省略されて必要なプロパティ名のみ残る
// 省略可能が含まれていると、プロパティ名を取得した結果にundefinedがついてしまうので、取り除く
type TypePropNames<T, Type> = TypeFilter<{ [P in keyof T]: ExtProps<T, P, Type> }[keyof T], undefined>;
type UserStrProps = TypePropNames<UserParams, string>;
// => 'name'|'sex'|'address'|'phone'|'zipCode'|'mail'

これで必要なプロパティ名のみを取得できました。

あとは、部分型を作れば達成できます。


省略可能を考慮して部分型を作る

単に部分型を作ると省略可能属性を引き継げないため、別にデフォルトが指定されているプロパティは省略可能にしたい特定の型のプロパティのみからなる部分型を作りたいに記述した方法を応用します。

基本的な発想は、省略可能なプロパティ名を抜き出し、型を2つに分けて足し合わせるです。

省略可能なプロパティはundefinedを含むかどうかで判定できます。


interface UserParams {
id: number;
name: string;
sex: string;
address?: string;
phone?: string;
zipCode?: string;
mail: string;
isDeleted?: boolean;
}

// 上で書いたものと同様
type TypeFilter<T, Type> = T extends Type ? never : T;
type ExtProps<T, P extends keyof T, Type> = T[P] extends TypeFilter<T[P], Type> ? never : P;
type TypePropNames<T, Type> = TypeFilter<{ [P in keyof T]: ExtProps<T, P, Type> }[keyof T], undefined>;

type SubType<T, P extends keyof T> = { [K in Exclude<P, TypePropNames<T, undefined>>]: T[K] } & { [K in Extract<P, TypePropNames<T, undefined>>]?: T[K] };
type UserSubType = SubType<UserParams, 'name' | 'address' | 'mail' | 'isDeleted'>;
// => {name: string; mail: string} & { address?: string|undefined; isDeleted?: boolean|undefined; }


inferの利用

TSはinferを使って、推論された結果の型を使用することができます。

一番利用頻度が高いのは関数の戻り型を取得することですが、組み込みでReturnTypeが存在するので説明は省きます。

説明が難しいので、具体例を2つほど紹介します。

※ 使う状況が限られているのでおまけです。


渡された関数の引数の型を取りたい


type ExtractArg<T> = T extends (arg1: infer A, ...rest: any[])=> any ? A : never;
const factory = (params: {type: 'text', placeholder?: string;}) => {
// ...実装
};
type TextParams = ExtractArg<typeof factory>;
// => {type: 'text', placeholder?: string|undefined;};

inferextendsの中でのみ使用可能で、型推論でそこに割り当てられた型を後で使うことを表します。

例のExtractArgは、Tが引数を1つ以上持つ何らかの関数だった場合、最初の引数をAに割り当てており、その型を返してくれます。

汎用的に、任意の個数の引数をとる関数を対象にある程度綺麗な形で取得したい場合、沢山並べる必要があります。


type Single<T> = T extends (arg1: infer A) => any ? [A] : never;
type Double<T> = T extends (arg1: infer A, arg2: infer B) => any ? [A, B] : never;
type Triple<T> = T extends (arg1: infer A, arg2: infer B, arg3: infer C) => any ? [A, B, C] : never;
type Quad<T> = T extends (arg1: infer A, arg2: infer B, arg3: infer C, arg4: infer D, ...rest: any[]) => any ? [A, B, C, D] : never;
// シグネチャが長いものは短いものを受け入れられるので、短い方からチェックする
type ExtractArgs<T> = Single<T> extends never ? Double<T> extends never ? Triple<T> extends never ? Quad<T> extends never ? never : Quad<T> : Triple<T> : Double<T> : Single<T>;

type YearMonth = ExtractArgs<(year: number, month: number)=>any>;
// => [number, number];
type YearType = YearDate[0];
// => number;

type YearMonthDate = ExtractArgs<(year: number, month: number, month: number)=>any>;
// => [number, number, number];
type All = ExtractArgs<(year: number, month: number, date: number, hour: number, minute: number, second: number>)=> any>;
// => 引数が何個でもQuadが該当するので、[number, number, number, number]

type NotEnough = Quad<(year: number, month: number)=>any>;
// => 足りない文は`{}`で補完されるため、[number, number, {}, {}]


配列から要素の型を取りたい


type ExtractElementType<T> = T extends (infer A)[] ? A : never;
type Result = ExtractElementType<(string|number)[]>;
// => string|number

上に比べて難しい話ではないですが、infer A[]だとエラーが出るので、適切にカッコをつける必要があります。

(地味にハマってました)