概要
TypeScript(以下TS)の型定義の最大の特徴は__型を計算して指定できる__ことだと思います。
この記事では、型計算を行うときの基本的な考え方を状況別にまとめました。
※特に複雑な型計算は、もっと簡単な記述方法があるかもしれませんが、ご了承ください
対象の環境
TypeScript 3.7.5
どのような人向きか?
- 基本的な文法は理解されている。
- やりたい定義は何となく浮かんでいるが、上手く書き起こせていない方。
- 公式のドキュメント読んだが、具体的な使い方が分からなかった方。
状況別の基本的やり方
複数の型を受け入れたい
|
を使うことで、型1または型2のいずれか
を表現できます。
type InputTypes = 'text'|'textarea';
type NullableTypes = InputTypes|null;
// => 'text'|'textarea'|null;
型を計算する際は、例のようにnull
やundefined
を足すケースが多いと思います。
型を除きたい
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;}
部分型を作りたいと型を除きたいの組み合わせです。
keyof
でT
のプロパティ名を全て取得できますので、順番に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;};
infer
はextends
の中でのみ使用可能で、__型推論でそこに割り当てられた型を後で使う__ことを表します。
例のExtractArg
は、Tが引数を1つ以上持つ何らかの関数だった場合、最初の引数をAに割り当てており、その型を返してくれます。
汎用的に、任意の個数の引数をとる関数を対象にある程度綺麗な形で取得したい場合、沢山並べる必要があります。
2020/01/22追記
気づいたら組み込みにParameters
が追加されていました。関数の引数を使いたい場合、Parameters<typeof targetFunction>[0]
のような形で取得できます。
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[]
だとエラーが出るので、適切にカッコをつける必要があります。
(地味にハマってました)