🎄

Parameters<T>, ConstructorParameters<T> / TypeScript一人カレンダー

2022/12/15に公開約6,300字

こんにちは、クレスウェア株式会社の奥野賢太郎 (@okunokentaro) です。本記事はTypeScript 一人 Advent Calendar 2022の5日目です。昨日は『実例 runRenderHook()』を紹介しました。

Parameters<T>

Parameters<T>はTypeScript 3.1から追加されたUtility Typesのひとつです。

https://www.typescriptlang.org/docs/handbook/utility-types.html#parameterstype

この型も以前紹介したinferを使います。なのでいきなり定義から見てみましょう。

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

ReturnType<T>inferを学んでから読むと、読みやすくなると思います。ReturnType<T>が関数の戻り値の型を返したのに対し、Parameters<T>は関数の引数の型を返すのが特徴です。

type T1 = Parameters<(v: string) => void>;
//   ^? [v: string]

type T2 = Parameters<(x: number, y: number) => number>;
//   ^? [x: number, y: number]

このとき、ReturnType<T>とは違ってLabeled Tuple Elementsを返すことに注意してください。Labeled Tuple ElementsというのはTypeScript 4.0から追加された概念で、Tupleにラベルを付与することができるというものです。

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#labeled-tuple-elements

そのため例のように[string]ではなく[v: string][number, number]ではなく[x: number, y: number]を得ることができます。

Labeled Tuple Elements

Labeled Tuple Elementsはライブラリの型定義ファイル作成時などに最適でして、業務で頻繁に使うようなものではありません。業務だと値に名前をつけた構造体を定義したければ、オブジェクトを採用したほうが圧倒的に値の取得時の使い勝手がよいですからね。もしかしたら4.0で追加されたことも気付いていない方もいるかもしれません。

用途として思いつくのは[lat, lon]の座標や、[r, g, b]のような、そこまでlengthが長くなく順番が(少なくとも開発者界隈で)一般的に知れ渡ったものなどではLabeled Tuple Elementsでも扱いやすいかもしれません。数学界隈やゲーム開発でもTupleを好まれがちという印象があります。

得た型の使い方

Parameters<T>からLabeled Tuple Elementsを得られるということは、引数ごとの型を扱いたい場合は添字を指定します。[v: string]型だからといってParameters<T>['v']とはできません。それができるのはオブジェクト型にvプロパティが定義されている場合です。

type T21 = Parameters<(v: string) => void>;
//   ^? [v: string]

type T22 = Parameters<(v: string) => void>[0];
//   ^? string

type T23 = Parameters<(v: string) => void>["v"];
// Error: Property 'v' does not exist on type '[v: string]'.(2339)

type T24 = { v: string }["v"];
//   ^? string

使う添字は第一引数に対してが0、第二引数に対してが1です。ECMAScriptの配列のインデックスと同じく0始まりです。

Indexed Access Types

もうひとつ、これは小技ですが[number]という指定もできます。こうするとすべてをUnion Typesとして得ることができます。試しにparseInt()を与えてみましょう。

type T31 = Parameters<typeof parseInt>;
//   ^? [string: string, radix?: number | undefined]

type T32 = T31[number];
//   ^? string | number | undefined

undefinedまでくっついてきました。面白いですね。[number]は、なにもParameters<T>のときに使うというわけではないですが、いずれの状況でもTuple TypesからUnion Typesに変換する手法として覚えておくとお得。知らないままだと偶然には思いつきにくい書き方です。

なお、この仕様はIndexed Access Typesといいます。

https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html

実は、先日紹介したUnArray<T>は、inferを使うことなく、この仕様を使っても解決できます。これに気付いていた読者はかなりのTypeScript通ではないでしょうか。

type UnArray<T> = T extends Array<infer A> | ReadonlyArray<infer A> ? A : never;

type T11 = UnArray<string[]>;
//   ^? string

type T12 = UnArray<number[][]>;
//   ^? number[]

type T13 = UnArray<UnArray<number[][]>>;
//   ^? number

type T14 = UnArray<Array<string>>;
//   ^? string

type T21 = string[][number];
//   ^? string

type T22 = number[][][number];
//   ^? number[]

type T23 = number[][number][][number];
//   ^? number

type T24 = Array<string>[number];
//   ^? string

どちらを使うかはもちろん好みですが、筆者はUnArray<T>の方が直感的だと思い、この状況での[number]は採用していません。

どんなときに便利?

Parameters<T>を使う場面といえば、真っ先に挙げられるのがReactのコンポーネントからPropsを参照する状況でしょう。例えばmuiを使っていたとして、そのコンポーネントが持つonClickの型を参照したい場合はこのようにします。

import { Button } from '@mui/material';

type Props = {
  onClick: Parameters<typeof Button>[0]['onClick'];
}

あなたの周りにはonClick: (ev: any) => voidなんて書き方はありませんか?ぜひParameters<T>を使ってみてください。Parameters<T>とReactのコンポーネント関数との相性は非常によいため、一度知ってしまうと今後の書き方に大きな影響を与えると思います。

ところでHTMLのbutton要素のonClickを取得したい場合、これはコンポーネント関数の扱いではないため次のように書くことができます。これもReact案件ではよくやる手法です。

import {
  type ButtonHTMLAttributes,
  type DetailedHTMLProps,
} from 'react';

type OnClick = DetailedHTMLProps<
  ButtonHTMLAttributes<HTMLButtonElement>,
  HTMLButtonElement
>['onClick'];

DetailedHTMLPropsとして定義されているので、こちらはParameters<T>を使いません。どちらにせよPropsの型定義を多重記述やDRY違反せずに取得できるというのは構造整理をする上で非常に有益です。他コンポーネントのProps型定義はコピペせずに参照する、これを心がけると依存関係が扱いやすくなります。

ConstructorParameters<T>

記事名に含めた割にすっかり余談扱いになっていますが、classconstructorの引数を取得するための手段としてConstructorParameters<T>という型も用意されています。

筆者はここ数年、TypeScript内でclassを扱うことがほぼなくなったためConstructorParameters<T>も使わないのですが、関連として紹介しておきます。

明日は『Pick<T, K>

ここまでinferを使ったいくつかのUtility Typesと実例を紹介してきました。どれも型推論を扱う上で重要なものばかりです。明日はそこから少し離れて別のジャンルのUtility TypesであるPick<T, K>を紹介します。それではまた。

Discussion

ログインするとコメントできます