前置き: Conditional Typesとは
※ご存知の方は読み飛ばしてください
Conditional Typesとは、条件分岐した型定義です。
T extends U ? X : Y
のような記法で、
特定の型を満たしているか を三項演算子で判定するのが基本形です。
簡単な具体例を示すと、以下のような形です。
type BoolOnly<T> = T extends boolean ? T : never
type Foo = BoolOnly<string> // never
type Bar = BoolOnly<boolean> // boolean
type Baz = BoolOnly<string | true> // true
Bazは Distributive conditional types と呼ばれる概念により分散評価されています。
他にも便利な使い方がありますが、今回のテーマから脱線してしまうので、割愛します。
概要は 公式ドキュメント 等をご覧ください。
本題: 型じゃなくて具体的な値で条件分岐したいなァ
前述の通り、条件として使えるのは、 特定の型を満たしているか です。
通常の比較演算子や論理演算子を用いることはできません。
その特性上、特定の型を変換したり、型の一部を抽出したり、といった使い方が汎用的です。
しかし、型ではなく具体的な変数や引数の値で条件分岐したいケースもあるかと思います。
そのような場合、値を 具体的な型を推論できる形 で定義した上で、
型を満たしているか で表現できる程度の条件であれば、Conditional Typesが利用できます。
例えば、 10以上の整数 といった、型で表現することが難しい条件には向きませんが、
特定の固定値を持つオブジェクト のようなケースであれば、具体的な型化・条件分岐ができます。
具体的な型の推論
TypeScriptにおいて、具体的な型を推論できる値は、概ね以下のような場合です。
-
const
で型指定せずに宣言された、プリミティブな値 -
as const
アサーション付きで型指定せずに宣言された、オブジェクトや配列 - ジェネリクスを用いて定義された関数の引数に、1, 2を渡した値
具体的な型の推論の成否パターンをいくつか例示します。
// good
const specificString = 'foobar' // : "foobar"
const specificObj = { someFlag: true } as const // : { readonly someFlag: true }
const satisfiedObj = { someFlag: true } as const satisfies { someFlag: boolean } // : { readonly someFlag: true }
const genericsPrimitiveArg = (<T extends boolean>(arg: T) => arg)(true) // : true
const specificStringsArg = (<T extends readonly string[]>(arg: T) => arg)(['foo', 'bar'] as const) // : readonly ["foo", "bar"]
const constTypeStringsArg = (<const T extends readonly string[]>(arg: T) => arg)(['foo', 'bar']) // : readonly ["foo", "bar"]
const directlyObjArg = (<T extends { someFlag: boolean }>(arg: T) => arg)({ someFlag: true }) // : { someFlag: true }
const specificVarObjArg = (<T extends { someFlag: boolean }>(arg: T) => arg)(specificObj) // : { readonly someFlag: true }
// bad
let fuzzyString = 'foobar' // : string
const fuzzyObj = { someFlag: true } // : { someFlag: boolean }
const typedObj: { someFlag: boolean } = { someFlag: true } as const // : { someFlag: boolean }
const typedArg = ((someFlag: boolean) => someFlag)(true) // : boolean
const fuzzyStringsArg = (<T extends readonly string[]>(arg: T) => arg)(['foo', 'bar']) // : string[]
const fuzzyVarObjArg = (<T extends { someFlag: boolean }>(arg: T) => arg)(fuzzyObj) // : { someFlag: boolean }
宣言時に型指定してしまうと、具体性が失われてしまうので注意です。
具体性を保ちつつ制約を持たせたい場合は satisfies operator を使いましょう。
関数の引数であれば const type parameters を活用すると、場合により安全性が高まります。
実践: Conditional Typesを具体的な値で条件分岐する
具体的な型の推論ができる値さえできていれば、あとは値を適切に型にしてConditional Typesで解釈するだけです。
基本的には、値を typeof
した型を型引数として渡します。
関数の場合は、ジェネリクスを適切に記載していれば、引数に値を渡すだけです。
例として、リクエストパラメータによってレスポンスが変わるAPIの型を考えてみます。
type SomeResponseBase = {
values: string[]
/** onlyCount が true の場合、このパラメータのみを返却 */
count: number
}
type SomeParams = {
onlyCount?: boolean
}
// 一般的な Conditional Types
/** Object の型をまるごと条件に指定 */
type ObjCondResponse<T> = T extends { onlyCount: true } ? Pick<SomeResponseBase, 'count'> : SomeResponseBase
/** 型引数に制約を持たせ Object のプロパティで条件判定 */
type PropCondResponse<T extends SomeParams> = T['onlyCount'] extends true ? Pick<SomeResponseBase, 'count'> : SomeResponseBase
// 関数の戻り値やジェネリクス型引数での Conditional Types
/** 仮のAPI実行関数 */
declare function callSomeApi<T = any>(params: SomeParams): T;
/** 関数の戻り値に Conditional Types */
const retCondFunc = <T extends SomeParams>(
params: T
): T['onlyCount'] extends true
? Pick<SomeResponseBase, 'count'>
: SomeResponseBase => {
const response = callSomeApi(params)
return response
}
/** ジェネリクス型引数に Conditional Types */
const genCondFunc = <T extends SomeParams>(params: T) => {
const response =
callSomeApi<
T['onlyCount'] extends true
? Pick<SomeResponseBase, 'count'>
: SomeResponseBase
>(params)
return response
}
/** 定義済みの Conditional Types を戻り値と型引数に利用 */
const definedCondFunc = <T extends SomeParams>(params: T): ObjCondResponse<T> => {
const response =
callSomeApi<ObjCondResponse<T>>(params)
return response
}
// Worked Conditional Types
const truthyParams = { onlyCount: true } as const satisfies SomeParams
const falsyParams = { onlyCount: false } as const satisfies SomeParams
const falsyEmptyParams = {} as const satisfies SomeParams
type Response1_1 = ObjCondResponse<typeof truthyParams> // { count: number; }
type Response1_2 = ObjCondResponse<typeof falsyParams> // { values: string[]; count: number; }
type Response1_3 = ObjCondResponse<typeof falsyEmptyParams> // { values: string[]; count: number; }
type Response2_1 = PropCondResponse<typeof truthyParams> // { count: number; }
type Response2_2 = PropCondResponse<typeof falsyParams> // { values: string[]; count: number; }
type Response2_3 = PropCondResponse<typeof falsyEmptyParams> // { values: string[]; count: number; }
const response1_0 = retCondFunc({ onlyCount: true }) // : Pick<SomeResponseBase, "count">
const response1_1 = retCondFunc(truthyParams) // : Pick<SomeResponseBase, "count">
const response1_2 = retCondFunc(falsyParams) // : SomeResponseBase
const response1_3 = retCondFunc(falsyEmptyParams) // : SomeResponseBase
const response2_0 = genCondFunc({ onlyCount: true }) // : Pick<SomeResponseBase, "count">
const response2_1 = genCondFunc(truthyParams) // : Pick<SomeResponseBase, "count">
const response2_2 = genCondFunc(falsyParams) // : SomeResponseBase
const response2_3 = genCondFunc(falsyEmptyParams) // : SomeResponseBase
const response3_0 = definedCondFunc({ onlyCount: true }) // : Pick<SomeResponseBase, "count">
const response3_1 = definedCondFunc(truthyParams) // : Pick<SomeResponseBase, "count">
const response3_2 = definedCondFunc(falsyParams) // : SomeResponseBase
const response3_3 = definedCondFunc(falsyEmptyParams) // : SomeResponseBase
// Not worked Conditional Types
const fuzzyParams = { onlyCount: true }
const typedParams: SomeParams = { onlyCount: true } as const
type BadResponse1_1 = ObjCondResponse<typeof fuzzyParams> // { values: string[]; count: number; }
type BadResponse1_2 = ObjCondResponse<typeof typedParams> // { values: string[]; count: number; }
const BadResponse1_1 = retCondFunc(fuzzyParams) // : SomeResponseBase
const BadResponse1_2 = retCondFunc(typedParams) // : SomeResponseBase
関数の引数にオブジェクトを渡す場合、変数経由でなく直接値そのものを渡しても具体的に推論されます。
ただ、 具体的な型の推論 の項目に記載の例にありますが、配列を直接渡す場合は as const
等が必要なので、要注意です。
まとめ
- Conditional Typesに限らず、具体的な型を推論したい場合は以下を使うと良いでしょう
- プリミティブな値: const宣言
- それ以外: as const satisfies *
- 宣言時に型指定すると具体性が失われるので注意しましょう
- Conditional Typesを具体的な値(型)で条件分岐することは可能ですが、値の定義方法によって結果が変わるので使い方に注意しましょう
- 条件分岐の書き方によっては、推論上は存在するはずのパラメータが実際は無い、ということが有りえます
- TypeScriptの型をテストできるライブラリの導入を検討したいところです