LoginSignup
37
25

More than 3 years have passed since last update.

TypeScript:関数の引数に同じ長さの配列を渡したい

Last updated at Posted at 2021-01-25

やりたいこと

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];

の時点でarrnumber[]と推測されてしまっているからです。

以下のように改善すると期待通りの挙動になります。


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であれば空のタプルを要求することでエラーを発生させています。

参考

37
25
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
37
25