2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

【TypeScript】ユニオン型を型を厳密に保ったまま別のユニオン型に変換する

Posted at

はじめに

色の名前"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"]の分岐に入り、
nullextends "red" | "green" | "blue"を満たさないので後半の: nullの分岐に入ります。

型の解説はここまでで、関数の実装部分についてもひとつ補足です。

const getHexNullable: GetHexNullable = (colorName) =>
  (colorName && hexByColorName[colorName]) as any;

返り値にas anyを使っています。
型定義の方を厳密にした影響でas anyをつけないとエラーになってしまうからですが、ここまでして帰り値の型を厳密にするかについては場合によると思います。

参考

Conditional Typeやas anyの扱いなどについて、以下の記事がとても参考になりました。

まとめ

なるべく型を厳密にしたいときはGenericsを使おう。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?