29
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

お題は不問!Qiita Engineer Festa 2023で記事投稿!

Conditional Typesを具体的な値で条件分岐するための型推論

Last updated at Posted at 2023-07-11

前置き: 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において、具体的な型を推論できる値は、概ね以下のような場合です。

  1. const で型指定せずに宣言された、プリミティブな値
  2. as const アサーション付きで型指定せずに宣言された、オブジェクトや配列
  3. ジェネリクスを用いて定義された関数の引数に、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の型をテストできるライブラリの導入を検討したいところです
29
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?