TypeScript の NoInfer と never で型引数の指定を必須にする
TypeScript 5.4 で追加された NoInfer
ユーティリティ型と never
型をうまく使うと、型引数が必須な関数を作ることができます。
const requireTypeParameter = <T = never>(args: NoInfer<T>) => {};
// 引数の型が never であるのに string の値を代入しようとしてエラーになる!
// Argument of type 'string' is not assignable to parameter of type 'never'.(2345)
requireTypeParameter('tomato');
// これらはエラーにはならない
requireTypeParameter<string>('tomato');
requireTypeParameter<'tomato' | 'onion'>('tomato');
こちらの Playground で実際に試すことができます。
この記事の内容は TypeScript v5.4.5 で動作することを確認しています。
動作の説明
この説明の各ステップはこちらの Playground で実際の動きを確認できます。
まずは次のような関数を考えてみましょう。
const withGenerics = <T>(args: T) => {};
この関数に引数に適当な値を渡すと型推論のおかげでいい感じの型で解釈してくれます。
もちろん、三つ目の例のように型引数を渡すこともできます。
withGenerics('hello');
// const withGenerics: <string>(args: string) => void
withGenerics(true);
// const withGenerics: <boolean>(args: boolean) => void
withGenerics<0 | 1>(1);
// const withGenerics: <0 | 1>(args: 0 | 1) => void
次にデフォルト型引数に never
を渡してみます。
const withGenericsDefaultNever = <T = never>(args: T) => {};
デフォルト型引数があっても型の推論が可能な場合には型推論が効くので先ほどの例と同様に適当な値を渡して型推論をさせられます。
withGenericsDefaultNever('hello');
// const withGenericsDefaultNever: <string>(args: string) => void
withGenericsDefaultNever(true);
// const withGenericsDefaultNever: <boolean>(args: boolean) => void
withGenericsDefaultNever<0 | 1>(1);
// const withGenericsDefaultNever: <0 | 1>(args: 0 | 1) => void
今度はデフォルト引数を渡さずに引数の型に NoInfer
を適用してみましょう。
const unknownTypeParameter = <T>(args: NoInfer<T>) => {};
unknownTypeParameter
に型引数を渡さない場合は T
は unknown
と解釈されます[1]。 あらゆる値は unknown
に代入可能なので、型引数を渡さなくてもエラーになりません。もちろん、型引数を渡して使用することもできます。
unknownTypeParameter('hello');
// const unknownTypeParameter: <unknown>(args: unknown) => void
unknownTypeParameter(true);
// const unknownTypeParameter: <unknown>(args: unknown) => void
unknownTypeParameter<0 | 1>(1);
// const unknownTypeParameter: <0 | 1>(args: 0 | 1) => void
最後にデフォルト引数に never
を渡しながら引数の型に NoInfer
を適用してみましょう。
const requireTypeParameter = <T = never>(args: NoInfer<T>) => {};
NoInfer
の機能で型推論がされなくなるので、型引数を指定しないとデフォルト型引数の never
として解釈されます。 あらゆる値は never
型ではないので、型エラーが発生するようになります。型引数を指定することで、その型の値を引数に渡せるようになります。
requireTypeParameter('hello');
// error: Argument of type 'string' is not assignable to parameter of type 'never'.(2345)
requireTypeParameter(true);
// error: Argument of type 'boolean' is not assignable to parameter of type 'never'.(2345)
requireTypeParameter<number>(1);
// const requireTypeParameter: <number>(args: number) => void
requireTypeParameter<0 | 1>(1);
// const requireTypeParameter: <0 | 1>(args: 0 | 1) => void
活用例
そもそも型推論は便利な機能なのであんまり活用したいケースはないんじゃないかなと考えていますが、なんとか頑張って活用してみましょう。たとえば option
タグを使った React のコンポーネントを実装には使えるかもしれません。
option
タグの value
属性に渡せる値は通常 string | number | readonly string[] | undefined
ですが、 NoInfer
と never
を使って型の指定を必須にし、その型の値以外は渡せないようにすることができます。
type DefaultValue = string | number | readonly string[] | undefined;
type Props<Value extends DefaultValue> =
Omit<React.ComponentPropsWithoutRef<'option'>, 'value'>
& { value: Value };
const Option = <Value extends DefaultValue = never>(props: Props<NoInfer<Value>>) =>
<option {...props} />;
このように実装しておくと、 Option
コンポーネントを使うときには型の指定が必須になります。型引数の指定を必須にすることで、指定しわすれが発生しないので、 value
属性に意図しない値を渡すリスクを回避することができます。
実際に次のように実装してみると、一つ目の例では型エラーが出ることがわかります。
二つ目の実装のように都度型を指定したり、三つ目のように型を指定したコンポーネントを宣言して使い回すことで value
属性に適当な値しか渡せない実装にすることができます。
// 取りうる値を表現する型
type Fruit = "apple" | "banana" | "cherry";
// 🙅 不適切な実装
// エラー: Type 'string' is not assignable to type 'never'.(2322)
<select>
<Option value="apple">りんご</Option>
<Option value="banana">バナナ</Option>
<Option value="cherry">さくらんぼ</Option>
</select>
// 🙆 適切な実装
<select>
<Option<Fruit> value="apple">りんご</Option>
<Option<Fruit> value="banana">バナナ</Option>
<Option<Fruit> value="cherry">さくらんぼ</Option>
{/* 指定した型の値以外は value に渡せない */}
<Option<Fruit> value="tomato">トマト</Option>
</select>
// 🙆 型引数に型を渡したコンポーネントを宣言してそれを使い回すこともできる
const FruitOption = Option<Fruit>;
<select>
<FruitOption value="apple">りんご</FruitOption>
<FruitOption value="banana">バナナ</FruitOption>
<FruitOption value="cherry">さくらんぼ</FruitOption>
</select>
こちらの Playground で実際に試すことができます。
-
NoInfer
が実装された PR の中でも言及されています。 PR の説明欄に "having constraint typeunknown
" とあるのは同じことを言っているのかもしれないなと思っていますが、あまりよくわからなかったです。 https://github.com/microsoft/TypeScript/pull/56794#issuecomment-1962387689 ↩︎
Discussion