TypeScript: 渡した値が返ることを保証したい

メモです

例えばstyle要素のように、渡せる値が限られているプロパティに動的な値を渡したいとします。

このとき、渡す型を明示しないで戻り値の型を判別可能な関数を用意します。 イメージとしては以下のような型を持つpositionに、indexに応じて動的に値を渡すことになります。

type Position = "absolute" | "static"
type Top = number
type Style = {position: Position; top: Top;}
const style: Style = {
  // error: Type 'number | "static"' is not assignable to type 'Position'.
  position: getStyle(index, {first: 100, second: "static"}),
  // ok
  top: getStyle(index, {first: 100, second: 50}),
}

上記の例では、getStyle()にはpositionとtopが取りうるPosition,Topどちらもを渡すことができますが、positionプロパティはPosition型のみを受け入れるのでgetStyleへ渡した数値100が返る可能性があるとエラーで教えてくれます。

1. Generics

TypeScript: Documentation - Generics

最初に思いついたGenericsを下記のように使ってみます。

const getStyle = <T extends StyleValue>(index, style: { first: T; second: T; }): T => { .. }
const style: Style = { position: getStyle(0, { first: "static", second: 0 })}
> Type 'number' is not assignable to type '"static"'.

もちろんgetStyle<Position | Top>(...)とすれば可能ですが、今回は渡した値によって返す型を決めたいのでこの方法は取りません。

2. プロパティごとにGenericsを用意するする

TypeScript: Documentation - Generics

Genericsは複数の型を渡せるので、それぞれのプロパティに用意してみます。

const getStyle = <A extends StyleValue, B extends StyleValue>(index,
  style: { first: A; second: B; }): A | B => { ... }

戻り値をUnion型で指定できるのでよさそうです。 しかしstyleへ渡すプロパティが増えていくと、A|B|C|D|...冗長化しそうです。

3. Indexed Access Types

TypeScript: Documentation - Indexed Access Types

冗長化を防ぐ方法として、Indexed Access Typesというものがあります。 これはその型のプロパティを呼ぶことで、その型を入手することが出来ます。 type User = { id: number; name: string; }という型を定義すると、User["id"]でidの型であるnumberを取得できます。 また、keyof UserでUser型が持つプロパティ名のUnion型を得ることが出来ます。それをUser型に渡すと以下ようになります。

User[keyof User] ≒ User["id"|"name"] ≒ User["id"] | User["name"] ≒ number | string
type StyleMap = {
  first: StyleValue;
  second: StyleValue;
};
const getStyle = <T extends StyleMap>(index, style: T): T[keyof StyleMap] => { ... };

プロパティの数だけ宣言するよりもスッキリました。 Indexed Access Typesでも、渡したstyleの各プロパティが持つ値のUnion型を返すことが出来ているのでよさそうです。