#ゆめみからの挑戦状 ★第7弾
最近株式会社ゆめみでは、 Twitter にて #ゆめみからの挑戦状 という企画を定期開催しています
ゆめみの社員がプログラミングに関する問題を出して、皆さんに引用リツートで答えていただく、というものです
今回はその第7弾を 型レベル で解けたのでその解説を書きます
PHPでの解説と回答例
まずは素直に書いてみる
type Year = 2022
type YearMonth<
Year extends number,
Months extends string[] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']
> =
Months extends [infer Month extends string, ...infer RemainingMonths extends string[]]
? [`${Year}${Month}`, ...YearMonth<Year, RemainingMonths>]
: []
type Out = YearMonth<Year>
// ^? type Out = ["202201", "202202", "202203", "202204", "202205", "202206", "202207", "202208", "202209", "202210", "202211", "202212"]
Months
が最初の1つ目の要素 string
と残りの月の string[]
に型推論できるとき、 ${Year}${Month}
で文字連結して、残りの月も再帰的に評価する
素直に解いてみましたがいくつかダサい箇所があります
2桁の0埋め型
'01'
'01'
のような2桁の0埋めをベタ書きしてしまっているところが、絶妙にダサいですね。
型レベルで2桁の0埋めをできるようにします
2桁の0埋めをするには、文字列が何桁か判定する必要があります
文字列が何桁か判定する
type Split<T extends string> =
T extends `${infer U}${infer V}` // 1つ目の文字と残りの文字列に型推論
? [U, ...Split<V>] // できたら1つ目の文字と、残りを再帰的に配列に変換する
: []
type A = Split<'文字列'>
// ^? type A = ["文", "字", "列"]
type StringLength<T extends string> = Split<T>['length']
type B = StringLength<'文字列'>
// ^? type B = 3
文字列のままでは桁数を得られないので、まずは文字の配列に変換します
その後、配列の length
プロパティを見て桁数を得ます
0埋めする
type Split<T extends string> =
T extends `${infer U}${infer V}`
? [U, ...Split<V>]
: []
type StringLength<T extends string> = Split<T>['length']
type TwoDegitZeroFill<T extends string> =
StringLength<T> extends 1
? `0${T}`
: T
type A = TwoDegitZeroFill<'1'>
// ^? type A = "01"
type B = TwoDegitZeroFill<'10'>
// ^? type B = "10"
TwoDegitZeroFill
型は単純で、与えられた文字列が1桁だった場合、左に0をつけてあげます。それ以外の場合はそのまま返します
2桁の0埋め型を組み込む
type Year = 2022
type Split<T extends string> =
T extends `${infer U}${infer V}`
? [U, ...Split<V>]
: []
type StringLength<T extends string> = Split<T>['length']
type TwoDegitZeroFill<T extends string> =
StringLength<T> extends 1
? `0${T}`
: T
type YearMonth<
Year extends number,
Months extends number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
> =
Months extends [infer Month extends number, ...infer RemainingMonths extends number[]]
? [`${Year}${TwoDegitZeroFill<`${Month}`>}`, ...YearMonth<Year, RemainingMonths>]
: []
type Out = YearMonth<Year>
// ^? type Out = ["202201", "202202", "202203", "202204", "202205", "202206", "202207", "202208", "202209", "202210", "202211", "202212"]
2桁の0埋め型ができたので多少型レベル感が増しました(?)がまだダサいですね
1から12までの配列を型レベルで生成する
1から12までの配列を作るには、要素が12個の配列を作ることと、要素の値をインクリメントすることができなければなりません
指定された長さの配列を生成する型
type ArrayForLength<
Length extends number,
Res extends undefined[] = []
> = Res['length'] extends Length // 指定した長さの要素になったら
? Res // 配列を返す
: ArrayForLength<Length, [...Res, undefined]> // それまで要素を1つずつ増やす
type A = ArrayForLength<3>
// ^? type A = [undefined, undefined, undefined]
関数にしてみるとこのような処理の評価をイメージするとわかりやすいかと思います
const arrayForLength = (length: number, res: undefined[] = []) =>
res.length === length
? res
: arrayForLength(length, [...res, undefined])
インクリメント型
型レベルでは数値の計算はできません
なので、配列の長さを使ってインクリメントします
type Increment<T extends number, U extends unknown[] = []> =
U['length'] extends T // 指定した長さになったら
? [...U, unknown]['length'] // 要素を1つ追加して、配列の長さを返す
: Increment<T, [...U, unknown]> // 再帰的に配列に要素を1つずつ詰める
type A = Increment<13>
// ^? type A = 14
再帰的に配列に要素を詰めていって、指定した長さになったら、最後に要素を1つ追加してその配列の長さを返します
関数で書くとこのようなイメージです
const increment = (t: number, u: undefined[] = []) =>
u.length === t
? [...u, undefined].length
: increment(t, [...u, undefined])
※ただしこの方法にはいくつか制限があります
// 配列の要素数がマイナスになることはないので無限ループしてしまう
type B = Increment<-1>
// Type instantiation is excessively deep and possibly infinite.
// 配列の要素数が浮動小数点数になることはないので無限ループしてしまう
type C = Increment<2.3>
// Type instantiation is excessively deep and possibly infinite.
// 数が大きすぎると型の再帰回数の制限に引っかかってしまう
type D = Increment<1000>
// Type instantiation is excessively deep and possibly infinite.
範囲の配列型
「指定された長さの配列を生成する型」と「インクリメント型」を合わせて指定された範囲の配列を作る型をつくります
type Increment<T extends number, U extends unknown[] = []> =
U['length'] extends T
? [...U, unknown]['length']
: Increment<T, [...U, unknown]>
type ArrayRange<
Start extends number,
End extends number,
Res extends number[] = []
> = Start extends End // Start と End が一致したら
? [...Res, Start] // 今までの配列に Start を追加して返す
: ArrayRange<Increment<Start>, End, [...Res, Start]> // 再帰的に Start をインクリメントしつつ配列に要素を詰める
type A = ArrayRange<1, 12>
// ^? type A = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
関数で書くとこのようなイメージです
const arrayRange = (start: number, end: number, res: number[] = []) =>
start === end
? [...res, start]
: arrayRange(start + 1, end, [...res, start])
範囲の配列型を組み込む(完成)
type Year = 2022
type Increment<T extends number, U extends unknown[] = []> =
U['length'] extends T
? [...U, unknown]['length']
: Increment<T, [...U, unknown]>
type ArrayRange<
Start extends number,
End extends number,
Res extends number[] = []
> = Start extends End
? [...Res, Start]
: ArrayRange<Increment<Start>, End, [...Res, Start]>
type Split<T extends string> =
T extends `${infer U}${infer V}`
? [U, ...Split<V>]
: []
type StringLength<T extends string> = Split<T>['length']
type TwoDegitZeroFill<T extends string> =
StringLength<T> extends 1
? `0${T}`
: T
type YearMonth<Year extends number, Months extends number[] = ArrayRange<1, 12>> =
Months extends [infer Month extends number, ...infer RemainingMonths extends number[]]
? [`${Year}${TwoDegitZeroFill<`${Month}`>}`, ...YearMonth<Year, RemainingMonths>]
: []
type Out = YearMonth<Year>
// ^? type Out = ["202201", "202202", "202203", "202204", "202205", "202206", "202207", "202208", "202209", "202210", "202211", "202212"]
Months
の初期値を ArrayRange<1, 12>
に変えて完成です!
余談
この記事を書いていて気づきましたが別なアプローチをしてもおもしろそうな部分がありました
type TwoDegitZeroFill<T extends string> =
T extends `${infer _}${infer _}${infer _}`
? T
: `0${T}`
「何かしらの文字と、何かしらの文字と、空文字列」に型推論できたら、そのまま返す、それ以外は0を付けて上げるでも、今回の場合表現できます
infer _
が3つも並んでいて強烈ですねw
感想
もとのお題がPHPの問題で、しかも制約の「処理はワンライナーで書かなければいけない(改行は可能)」を無視してしまっているので、完全にレギュレーション違反です
しかしコードを実行せずに型レベルで解けるTypeScriptの型システムはとてもおもしろいと思いました
これからも隙きあらば #ゆめみからの挑戦状 を型レベルで解いてみようと思います!