概述
类型体操 是关于 TS 类型编程的一系列挑战(编程题目)。通过这些挑战,我们可以加深对 TS 类型系统的理解,举一反三,在实际工作中解决类似问题。
本文是我对 9155 ValidDate 的解题笔记。这个题不难,但有一些点比较有意思。
本文的读者是正在做 TS 类型体操,对上述题目感兴趣,希望获得更多解读的同学。读者需要具备一定的 TS 基础知识,这部分推荐阅读另一篇优秀博文 Typescript 类型编程,从入门到念头通达😇。
题目和答案
先直接贴出题目与答案,呈现问题全貌。
题目
Implement a type ValidDate
, which takes an input type T and returns whether T is a valid date.
Leap year is not considered
Good Luck!
ValidDate<'0102'> // true
ValidDate<'0131'> // true
ValidDate<'1231'> // true
ValidDate<'0229'> // false
ValidDate<'0100'> // false
ValidDate<'0132'> // false
ValidDate<'1301'> // false
答案
type DaysOfFeb = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28
type DaysOfSM = DaysOfFeb | 29 | 30
type DaysOfBM = DaysOfSM | 31
type MonthDays = {
1: DaysOfBM,
2: DaysOfFeb,
3: DaysOfBM,
4: DaysOfSM,
5: DaysOfBM,
6: DaysOfSM,
7: DaysOfBM,
8: DaysOfBM,
9: DaysOfSM,
10: DaysOfBM,
11: DaysOfSM,
12: DaysOfBM,
}
type Months = keyof MonthDays
type Format<T extends number | string> =
T extends unknown
? `${T}` extends `${1|2|3|4|5|6|7|8|9}`
? `0${T}`
: `${T}`
: never
type ValidDates<M extends Months = Months> =
M extends unknown
? `${Format<M>}${Format<MonthDays[M]>}`
: never
type ValidDate<T extends string> = T extends ValidDates ? true : false
解题思路
这个题有两个解题方向:
- 从输出中解析出月和日,然后去做逻辑判断
- 依据底层逻辑生成所有的可能结果,然后用输入去进行匹配
大多数解答是采用的第一种解题思路。我这里采用的是第二种解题思路,理由是:
- 可行性:valid dates 的总数只有 365 种,有限,而且量不大
- 易于理解:可以将数据、底层逻辑、生成逻辑比较干净拆分开,各部分内聚,容易理解和维护
准备数据
以下是准备的数据,很纯粹,不带格式,不带技术细节,只蕴含了业务规则(有哪些月份,各自有多少天),很容易理解和维护:
type DaysOfFeb = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28
type DaysOfSM = DaysOfFeb | 29 | 30
type DaysOfBM = DaysOfSM | 31
type MonthDays = {
1: DaysOfBM,
2: DaysOfFeb,
3: DaysOfBM,
4: DaysOfSM,
5: DaysOfBM,
6: DaysOfSM,
7: DaysOfBM,
8: DaysOfBM,
9: DaysOfSM,
10: DaysOfBM,
11: DaysOfSM,
12: DaysOfBM,
}
type Months = keyof MonthDays
格式化逻辑
为了将数字或字符串格式化为 2 位且左侧填充 0 的字符串,我们需要一个 Format
工具类型:
type Format<T extends number | string> =
T extends unknown
? `${T}` extends `${1|2|3|4|5|6|7|8|9}`
? `0${T}`
: `${T}`
: never
T extends unknown
是额外添加的,是为了让 Format
支持 union 类型的输入,所以强行利用了 distributive conditional type 特性。
生成所有 valid dates
type ValidDates<M extends Months = Months> =
M extends unknown
? `${Format<M>}${Format<MonthDays[M]>}`
: never
ValidDates
是一个所有 valid dates 字符串的 union。之所以做成一个泛型,还是为了利用 distributive conditional type,这样,M
在表达式中表达的就是所有 Months
中的一个具体的月份。
由于上面我们优化过 Format
,它支持直接处理 union,所以 Format<MonthDays[M]>
这里用起来就非常方便了。
大功告成
好了,现在来实现 ValidDate
就非常容易了:
type ValidDate<T extends string> = T extends ValidDates ? true : false
直接判断输入的 T
是否 extends ValidDates
即可,也不需要去做任何格式解析。