例えばこのように書いたら
ts
1type Input = {
2 [key: string]: string[] | boolean[];
3};
4
5type UnpackInput<T extends Input> = {
6 [K in keyof T]: T[K][number];
7};
8
9function unpack<T extends Input>(_input: T): UnpackInput<T> {
10 return {} as any;
11}
12
13const result = unpack({
14 str: ["aa", "bb"],
15 bol: [true],
16});
こうなることを期待しますが、
ts
1result.str; // (property) str: "aa" | "bb" <- こうはならない
2result.bol; // (property) bol: true
実際はstrの中身が不変であることを保証していないため、stringとして扱われてしまいます。
ts
1result.str; // (property) str: string <- stringにされてしまう
2result.bol; // (property) bol: true
一番シンプルな方法は引数の配列の型をreadonly
修飾子でReadonlyArray
にし、関数を呼び出す際に配列にas const
と書き加えることです。
ts
1type Input = {
2 [key: string]: readonly string[] | readonly boolean[];
3};
4
5type UnpackInput<T extends Input> = {
6 [K in keyof T]: T[K][number];
7};
8
9function unpack<T extends Input>(_input: T): UnpackInput<T> {
10 return {} as any;
11}
12
13const result = unpack({
14 str: ["aa", "bb"] as const,
15 bol: [true],
16});
17
18result.str; // (property) str: "aa" | "bb" <- "aa" | "bb" を残してくれる
19result.bol; // (property) bol: true
しかし関数を呼び出す度にas const
と書き加えるのは面倒ですし、万が一書き忘れたとしてもコンパイラは注意してくれません。そこで、もう一つの選択肢としてtscの特性を悪用活用したNarrow型ハックと呼ばれる方法があります。
ts
1
2type Narrow<T> = { [K in keyof T]: Narrow<T[K]> } | T;
Narrow型を使うことでオブジェクトの中身にreadonly
修飾子をつけ、呼び出し時にas const
アサーションをつけた時のような結果を得ることができます。
ts
1type Input = {
2 [key: string]: string[] | boolean[];
3};
4
5type UnpackInput<T extends Input> = {
6 [K in keyof T]: T[K][number];
7};
8
9function unpack<T extends Input>(input: Narrow<T>): UnpackInput<T> {
10 return {} as any;
11}
12
13const result = unpack({
14 str: ["aa", "bb"],
15 bol: [true],
16});
17
18result.str; // (property) str: "aa" | "bb" <- "aa" | "bb" を残してくれる
19result.bol; // (property) bol: true
黒魔術に抵抗がなければ後者をお勧めしますが、そうでなければeslint用のルールを書いてas const
を繰り返し書く方法が良いでしょう。
追記
上記Narrow型は例としてのシンプルさを優先するため、あらゆるエッジケースについて一才の考慮をしていません。実際の開発ではmillsp/ts-toolbeltにあるような真面目な実装を用いることをお勧めします。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2021/11/09 10:22