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

# 前置き: Conditional Typesとは
※ご存知の方は読み飛ばしてください
Conditional Typesとは、条件分岐した型定義です。
`T extends U ? X : Y` のような記法で、
**特定の型を満たしているか** を三項演算子で判定するのが基本形です。

簡単な具体例を示すと、以下のような形です。
```TypeScript
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** と呼ばれる概念により分散評価されています。
他にも便利な使い方がありますが、今回のテーマから脱線してしまうので、割愛します。
概要は [公式ドキュメント](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html) 等をご覧ください。


# 本題: 型じゃなくて具体的な値で条件分岐したいなァ
前述の通り、条件として使えるのは、 **特定の型を満たしているか** です。
通常の比較演算子や論理演算子を用いることはできません。
その特性上、特定の型を変換したり、型の一部を抽出したり、といった使い方が汎用的です。

しかし、型ではなく具体的な変数や引数の値で条件分岐したいケースもあるかと思います。
そのような場合、値を **具体的な型を推論できる形** で定義した上で、
**型を満たしているか** で表現できる程度の条件であれば、Conditional Typesが利用できます。

例えば、 **10以上の整数** といった、型で表現することが難しい条件には向きませんが、
**特定の固定値を持つオブジェクト** のようなケースであれば、具体的な型化・条件分岐ができます。


# 具体的な型の推論
TypeScriptにおいて、具体的な型を推論できる値は、概ね以下のような場合です。
1. `const` で型指定せずに宣言された、プリミティブな値
2. `as const` アサーション付きで型指定せずに宣言された、オブジェクトや配列
3. ジェネリクスを用いて定義された関数の引数に、1, 2を渡した値

具体的な型の推論の成否パターンをいくつか例示します。
```TypeScript
// 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** を活用すると、場合により安全性が高まります。

- [The satisfies Operator (TypeScript 4.9)](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html#the-satisfies-operator)
- [const Type Parameters (TypeScript 5.0)](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#const-type-parameters)


# 実践: Conditional Typesを具体的な値で条件分岐する
具体的な型の推論ができる値さえできていれば、あとは値を適切に型にしてConditional Typesで解釈するだけです。
基本的には、値を `typeof` した型を型引数として渡します。
関数の場合は、ジェネリクスを適切に記載していれば、引数に値を渡すだけです。

例として、リクエストパラメータによってレスポンスが変わるAPIの型を考えてみます。
```TypeScript
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の型をテストできるライブラリの導入を検討したいところです
      - [DefinitelyTyped-tools](https://github.com/microsoft/DefinitelyTyped-tools)
      - [tsd](https://github.com/SamVerschueren/tsd)
      - [Vitest](https://vitest.dev/guide/testing-types.html)