🧩

TypeScript の NoInfer と never で型引数の指定を必須にする

2024/04/27に公開

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 で実際に試すことができます。
https://www.typescriptlang.org/play/?#code/MYewdgzgLgBATgUwI4FcCWiAqBPADggBQEM4iBbBKBOGAXhgB5M6YwEA3agPgAoSBzCAC4YAORABJMADNqTLgEo6XGAG8AvgG4AUNoD0emNTgg4IgIJx+KCmFghpMKHgQwA5NDhow-NzDQQrCCwRBAQaPxgRABGADauUCAwuCTklNQwDk4u7myccG4AdDwATADMACwArAraiKgYCDj4xKQUVHA8bolkRIluCjp1yOhYLq1pHQye3vy83SC9-YPDDWMtqe1yC0sgfgA+7uBo4G7zPX17K0A

この記事の内容は TypeScript v5.4.5 で動作することを確認しています。

動作の説明

この説明の各ステップはこちらの Playground で実際の動きを確認できます。

https://www.typescriptlang.org/play/?#code/PTAEFpK7QdwSwC4AsD2BXRoAmBTAZgIboA2WiAngA66hWEBOhAtrorgwDRxJqagA5VAEkAdvg4AoSQGNUogM5YEKAOK5RHeDIWgAvKAA8AFU4A+ABSMA5goBcoYwEp9Z0AG8AvtJXJ1mhm0FCwByZFwSElQQpwBuUBBQOUVlXn8tHQdDJUDRa0sbe1Ac+DyXPTcAN1R4bElfdMCdC0QGdFwXROSlHjUNDKLDACNUVBJcQlEChlsHEbGJ0XKqmrqG-qaFQwAGUAAfUABGS0POsG7UvoCgrN2D46sZoruj5dBq2ulJROhf8F7kDgCMQyKBKDQ6IwWGwOKBNJUONxfBgsEIxBIGNILgDGkEACLA0iIAS4BEMfRGYwU+EcaazRxvLyxHxpDb4wlkElk0LhSLROIJc7yHrra46AlEIlcjhZEp5OlFOXWN4fNassUKCUg4mkjgtNodeJdYWXPxs8UcnVkrLzcaTBVzUZ2paud6rerqgZaqW6hg7fZHE4C40pHHmzWW6UMW4Bh6FBwvU6u1VfH5-KAAlFAyWg8G0ehMVjsLgAwQicRSWQm0DoUQAa1EqFgomM1FwAAUoUXYQYTOZHvS0RW-cYzIzPMzJLWG02W23O4WYQweREojEjULQ9PG83WzQF9Di1lt7OHTX6zuXRU3Z8T7v512l-r2sHNz073P94+j0YP2ePyq7ofnuHbfhw-r3EGG5JNWwEPouP4QYGA7PLGgGfNIabpv8vjZtqYJtpCCGwjSJa4UOGIXFi1YMLgACO6DwLRIEHt25K9lSBikWeFHgaO46TrRDFMbgLFgcuYSrvy0EcAwqDRqAACCMzoKwohYKg+AERCIRKiEoDwLojZYIQCgKPA1iiIQQzjGCqBEYesKadptAhKRIQAHQWAATAAzAALAArE4khCYxzHwY5y6tC+MkMHJCnKdYqkaBpWl5qAIS2os+mGXCqAmWZFlWTZtCIPZBZRaAzkZW5vqeT5AXBdIYUiWJxF+qIqlDLSFinNB2KtRFX4dVkXXMD1DBnuNk3oXUQ2iZFbFIQ8-WCjBoYLe1UUxpBKEJmhybupIQA

まずは次のような関数を考えてみましょう。

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 に型引数を渡さない場合は Tunknown と解釈されます[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 ですが、 NoInfernever を使って型の指定を必須にし、その型の値以外は渡せないようにすることができます。

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 で実際に試すことができます。
https://www.typescriptlang.org/play/?#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wG4AocgeirgGFdIA7JJmSmuAZxRCTgDuwGAAsAXHBgBPMPwAiSTCgCuAGxgA1FKuX8AvNxhRgTAOZwAPnCbKQAIyRRLcIigAmEJqqmHjZgNoAus7KTG6KJkhuFNKycApKapraunAGyOgwAHQM4J6sMAAKOGBcAOrCIhDKMMiYADz4EGAwwJ74AHz++ABuKUj4gRSUsfzFzVz1Wjr8SAAeMKxuXPGKKurTuh1p5HB7cADyIML1GRg5jPls46UVotW1io3Nre0dADQEfTOdu3AAZHAAN5wb66CSbfgAX2GaE8XHgBxabSYaTgU36cHmizCKwS62SMzRLB6jg6AAowCUuBIbpMAHIQACSTEwjgxMw6HQAlGkOn96i8UcCsqKqRMoXAqB1htRaAAJCACSQQODKLhIEYyfgAMSgymEaIARCgwGBVEgjc4jXYUEw7SgrVYjWgRI4oFIjbD4fAWd9gG4meALXw2ChXqiDOTeXptkC-kQYMooKjyX89vUNRaMPz9vsgVQAFRwAAqbo1cFUkRWaBUFfd0BWhaoUPT+aLpe1BARflM+DgwBWTAg8BQXC4wFM9rsFpVki7+BJjnwWXJACYAMxrte85utvMZpER0H9PQms0Wo0dACCF6Q9SoR5RuYP9SfnhPMzPtvt9qvACEHXtB93yYF88zfZEPzBJAz1dd1PQ6Og3SgD0QKgsC2wfLMkBzP5uQoVtyDhJgETgABxCAIEDYMkFDGBw2FKMYzjBMkCTFM4DTCCcLwg90VA+o9QNGBthgs9TXNS0bzvdCI3A-ZIIjIT9WEMTTxtIDHQ6QDfxQOTnzbQ8MJUkT1K-F0UI9K9kIQgzPH5IzgQ7MtBzgWt1SQFY7SxVDoDgPcnILYsSwXI0YFwcMICNfs3OHUdx0nadZwi+c4nwYThBXdctx3AKWycpSUVMtTP10M8IpAKKrxLSKIvszCIKoXjRPw8giJIsjKOooMpPoxjPDXNFoz5YE-k6+BMpgUC0UEqaZTYjjUywlqFIzKaZvE88pKvW8pIfDaMLW9FDuPLafwdACtIO1TpqOwrTuFLb4NQxDbNem6RNAhTsKQbNWr2bl2vIIA

脚注
  1. NoInfer が実装された PR の中でも言及されています。 PR の説明欄に "having constraint type unknown" とあるのは同じことを言っているのかもしれないなと思っていますが、あまりよくわからなかったです。 https://github.com/microsoft/TypeScript/pull/56794#issuecomment-1962387689 ↩︎

Discussion