やりたいこと
requireTwoSameLengthArrays([1, 2], [3, 4]); // ok
requireTwoSameLengthArrays([1, 2], [3]); // 型エラー
requireTwoSameLengthArrays([1, 2], [3, 4, 5]); // 型エラー
になるような関数を作りたい
結論
function requireTwoSameLengthArrays<
T extends (readonly [] | readonly any[]) & (
number extends T["length"] ? readonly [] : unknown
)>(t: T, u: { [K in keyof T]: any }){
//
}
ステップ1:最低限の解答
function requireTwoSameLengthArrays<
T extends [] | any[]
>(t: T, u: { [K in keyof T]: any }) {
//
}
解説
T extends [] | any[]
の箇所について。any[]
は[]
も内包しているので
T extends any[]
だけでいいのではないか?と思うかも知れませんが,これはコンパイラーに固定長のタプル型として認識させるために両方必要です。
ジェネリクスの配列・タプルの例
function func1<T extends any[]>(arg: T) {
return arg;
}
const a = func1([1,2,3]) //a: number[]
const b = func1([1,2,3] as const) //エラー: The type 'readonly [1, 2, 3]' is 'readonly' and cannot be assigned to the mutable type 'any[]'.
function func2<T extends readonly any[]>(arg: T) {
return arg;
}
const c = func2([1,2,3]) //c: number[]
const d = func2([1,2,3] as const) //d: readonly [1, 2, 3]
function func3<T extends any[] | []>(arg: T) {
return arg;
}
const e = func3([1,2,3]) //e: [number, number, number]
そのため,1つ目の引数tに[1,2]
が入ると,Tは[number,number]
であると推論されます。
u: { [K in keyof T]: any }
はmapped typeと呼ばれますが,Tに配列を入れると少し変わった挙動をして(Mapped Tuple Typeと呼ばれます),uはTの配列長と同じ(今回の場合は配列長2)のany[]
型となります。
Mapped Tuple Typeについて詳しく
公式の記事はこちら
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-1.html
以下の例のように,1つ1つの要素に対してマッピングされ,新しいタプルや配列を生成します。
type MapToPromise<T> = { [K in keyof T]: Promise<T[K]> };
type Coordinate = [number, number];
type PromiseCoordinate = MapToPromise<Coordinate>; // [Promise<number>, Promise<number>]
そこで,今回使われているように
const a: {[K in keyof [number,number]]: string}
としたときのaの型を見てみると,以下のように配列長が2のstring[]
型になっていることが確認できます。
const a: {
[x: number]: string;
0: string;
1: string;
length: string;
toString: string;
toLocaleString: string;
pop: string;
push: string;
concat: string;
join: string;
reverse: string;
shift: string;
slice: string;
sort: string;
... 20 more ...;
flat: string;
}
ステップ2:さらに拡張性のある解答
今のままだと以下のときにエラーが発生します。
requireTwoSameLengthArrays([1, 2] as const, [3, 4]);
//エラー:The type 'readonly [1, 2]' is 'readonly' and cannot be assigned to the mutable type 'any[]'.
以下のように改善するとエラーが発生しなくなります。
function requireTwoSameLengthArrays<
T extends readonly [] | readonly any[]
>(t: T, u: { [K in keyof T]: any }) {
//
}
ステップ3:さらにさらに拡張性のある解答
今のままでは以下のような問題があります。
const arr = [1, 2];
requireTwoSameLengthArrays(arr, [1, 2, 3]); //型エラーが起きてほしいのに起きない!!!!!!!
これは
const arr = [1, 2];
の時点でarr
がnumber[]
と推測されてしまっているからです。
以下のように改善すると期待通りの挙動になります。
function requireTwoSameLengthArrays<
T extends (readonly [] | readonly any[]) & (
number extends T["length"] ? readonly [] : unknown
)>(t: T, u: { [K in keyof T]: any }){
//
}
これを使うと以下のような挙動をします。
const arr = [1, 2];
requireTwoSameLengthArrays(arr, [1, 2, 3]);
// エラー:Argument of type 'number[]' is not assignable to parameter of type '(readonly any[] | readonly []) & readonly []'.
const arr = [1, 2] as const;
requireTwoSameLengthArrays(arr, [1, 2, 3]);
// エラー:Argument of type '[number, number, number]' is not assignable to parameter of type 'readonly [any, any]'.
const arr = [1, 2, 5] as const;
requireTwoSameLengthArrays(arr, [1, 2, 3]); //ok
解説
TがT["length"]
を持っているかどうかをチェックして、もしT["length"]
がnumber
であれば空のタプルを要求することでエラーを発生させています。
参考