無限に時間がかかるんじゃないかって思うぐらい苦戦した
多次元配列の定義
export type MultiDimensional<T> = T | MultiDimensional<T>[];
厳密には配列ではないですが(T
自体を含むため)、これを定義とします。
何がしたいか
例えば、以下のような型があるとします。
type STRINGS = [string,string,[[string],string],[string,string],string];
これを以下に変換したいのです。
type NUMBERS = [number,number,[[number],number],[number,number],number];
やりたいことは伝わったと思います。
多次元配列の定義そのままではだダメなのか
ダメです。せっかくTypeScriptやってるのに、多次元配列の構造を定義しないのであれば私には生きている価値がありません。
結論
以下でできます。
type SameStructure<
T,
M extends MultiDimensional<unknown>,
N extends 0[] = [],
> = M extends unknown[]
? N["length"] extends M["length"]
? []
: M[N["length"]] extends unknown[]
? [SameStructure<T, M[N["length"]]>, ...SameStructure<T, M, [...N, 0]>]
: [T, ...SameStructure<T, M, [...N, 0]>]
: T;
解説
行ごとに解説していきます。
引数
type SameStructure<
T,
M extends MultiDimensional<unknown>,
N extends 0[] = [],
>
-
T
: 変換先の型 -
M
: 変換元の構造(型は問わない) -
N
: カウンタ用配列
カウンタ用配列とは
TypeScriptの型では、数値の演算が基本的にできません。しかし、配列の長さで数値を表すと、インクリメントが簡単に表現できます。
~1行目
type SameStructure<
T,
M extends MultiDimensional<unknown>,
N extends 0[] = [],
> = M extends unknown[]
? ...
: T;
まず、M
が配列であるかどうかを見ます。配列であれば、次の処理に進み、そうでないなら、型T
をそのまま返します。
~2行目
type SameStructure<
T,
M extends MultiDimensional<unknown>,
N extends 0[] = [],
> = M extends unknown[]
? N["length"] extends M["length"]
? []
: ...
: T;
次に、再帰が終わったかどうかを判定します。
ここで重要なのが、N["length"] extends M["length"]
であることです。これは、暗黙的にM
のlength
が数値リテラル型であることを強制します。
逆に、M["length"] extends N["length"]
だと、M["length"]
が数値リテラル型でない、つまりnumber
型のときに、無限ループに陥ってしまいます。
再帰が終了したら、空配列を返すようにしています。
~3行目
type SameStructure<
T,
M extends MultiDimensional<unknown>,
N extends 0[] = [],
> = M extends unknown[]
? N["length"] extends M["length"]
? []
: M[N["length"]] extends unknown[]
? ...
: ...
: T;
3行目では、今見ているMの要素が配列であるかどうかを判別しています。それだけです。
~4行目
type SameStructure<
T,
M extends MultiDimensional<unknown>,
N extends 0[] = [],
> = M extends unknown[]
? N["length"] extends M["length"]
? []
: M[N["length"]] extends unknown[]
? [SameStructure<T, M[N["length"]]>, ...SameStructure<T, M, [...N, 0]>]
: ...
: T;
4行目では、今見ているMの要素が配列であったときに、再帰的にこの型を適用しています。
1つ目の再帰で、スタックを遡り、2つ目の再帰で、次の要素から見ていくようになっています。
~最後
type SameStructure<
T,
M extends MultiDimensional<unknown>,
N extends 0[] = [],
> = M extends unknown[]
? N["length"] extends M["length"]
? []
: M[N["length"]] extends unknown[]
? [SameStructure<T, M[N["length"]]>, ...SameStructure<T, M, [...N, 0]>]
: [T, ...SameStructure<T, M, [...N, 0]>]
: T;
最後に、今見ているMの要素が配列でないときに、T
とその次以降の要素を列挙して終わりです。
簡単でしょ?
1行毎に見ていけば全然難しくないです。が、これを組むのは意外と大変です。
試しにChatGPTで生成させてみようとしましたが、固定次元の多次元配列についてしか考えてくれませんでした。
論理的かつ再帰的な思考が必要なタスクはまだ難しいようです。
以上!