はじめに
色の名前"red" | "green" | "blue"
を対応するカラーコード"ff0000" | "00ff00" | "0000ff"
に変換する関数getHex
を考えます。
type ColorName = "red" | "green" | "blue";
const hexByColorName = {
red: "ff0000",
green: "00ff00",
blue: "0000ff",
} as const satisfies Record<ColorName, string>;
const getHex1 = (colorName: ColorName) => hexByColorName[colorName];
ではgetHex1
を使ってみます。
const hex1 = getHex1("red");
// hex1の型: "ff0000" | "00ff00" | "0000ff"
hex1
の型は"ff0000" | "00ff00" | "0000ff"
になってしまいます。
getHex1
関数には直接"red"
を渡しており他の値が渡る可能性はないので型は"ff0000"
となってほしいのが人情ですよね。
これを改善します。
結論
以下のようにすると期待通りになります。
const getHex2 = <T extends ColorName>(colorName: T) => hexByColorName[colorName];
const hex2 = getHex2("red");
// hex2の型: "ff0000"
引数にGenericsを使うようにしました。
これによってgetHex2
に渡した値"red"
は、文字列リテラルのユニオン型"ff0000" | "00ff00" | "0000ff"
ではなく厳密に文字列リテラル"red"
として扱われるようになります。
参考までに、型と実装を分けて書くと以下のようになります。
type HexByColorName = typeof hexByColorName;
type GetHex2 = <T extends ColorName>(colorName: T) => HexByColorName[T];
const getHex2: GetHex2 = (colorName) => hexByColorName[colorName];
【応用】引数にnull,undefinedを渡せるようにする
応用です。
引数としてnull
, undefined
が渡された場合は変換せずそのままnull
, undefined
を返すという要件を追加した関数getHexNullable
を作ってみます。
つまり以下のような感じです。
const nonNullHex = getHexNullable("red" as ColorName);
// const nonNullHex: "ff0000" | "00ff00" | "0000ff"
const nullableHex = getHexNullable("red" as ColorName | null);
// const nullableHex: "ff0000" | "00ff00" | "0000ff" | null
const nll = getHexNullable(null)
// const nll: null
そもそもnullなどは利用側でハンドリングするべきな気もしますが例ということで大目にみてくれると助かります。
では実装です。
type GetHexNullable = <T extends ColorName | null | undefined>(
colorName: T
) => T extends keyof HexByColorName ? HexByColorName[T] : T;
const getHexNullable: GetHexNullable = (colorName) =>
(colorName && hexByColorName[colorName]) as any;
実装はこれで完了ですが、簡単に解説です。
まずは型定義の返り値の部分について。
T extends keyof HexByColorName ? HexByColorName[T] : T
これはユニオン型とextends
(Conditional Type)を組み合わせる使い方です。
TypeScriptのドキュメントではこれをDistributive Conditional Types(分配条件型)と呼んでいます。
この意味はユニオン型T
のそれぞれの要素を別々に考えてみると分かりやすいです。
type HexByColorName = {
readonly red: "ff0000";
readonly green: "00ff00";
readonly blue: "0000ff";
}
// T extends keyof HexByColorName ? HexByColorName[T] : T
"red" extends "red" | "green" | "blue" ? HexByColorName["red"] : "red"
null extends "red" | "green" | "blue" ? HexByColorName[null] : null
上のようなイメージです(TypeScriptとしては正しくありません)。
"red"
はextends "red" | "green" | "blue"
を満たすので前半の? HexByColorName["red"]
の分岐に入り、
null
はextends "red" | "green" | "blue"
を満たさないので後半の: null
の分岐に入ります。
型の解説はここまでで、関数の実装部分についてもひとつ補足です。
const getHexNullable: GetHexNullable = (colorName) =>
(colorName && hexByColorName[colorName]) as any;
返り値にas any
を使っています。
型定義の方を厳密にした影響でas any
をつけないとエラーになってしまうからですが、ここまでして帰り値の型を厳密にするかについては場合によると思います。
参考
Conditional Typeやas any
の扱いなどについて、以下の記事がとても参考になりました。
まとめ
なるべく型を厳密にしたいときはGenericsを使おう。