进度(145 /188)
其中标记 ※ 的是我认为比较难或者涉及新知识点的题目
刷题也许没有什么意义,但是喜欢一个人思考一整天的灵光一现,也喜欢看到新奇的答案时的恍然大悟,仅此而已。
系列文
91. Medium - 18142 - All
如果传入的第一个参数中所有元素都等于传入的第二个参数,则返回 true;如果有不匹配,则返回 false。
// 遍历
type All<T extends any[], U> =
T extends [infer F, ...infer R]
? Equal<F, U> extends true
? All<R, U>
: false
: true
// 利用联合类型的分布式
type All<T extends any[], N, U = T[number]> =
false extends (U extends any ? Equal<U, N> : never) ? false : true
92. Medium - 18220 - Filter
实现类型 Filter<T, Predicate> 接受一个数组类型 T,原始类型或者原始类型的联合类型 P 返回 T 中包含 P 的元素组成的数组。
type Filter<T extends any[], P> =
T extends [infer F, ...infer R]
? F extends P
? [F, ...Filter<R, P>]
: Filter<R, P>
: []
93. Medium - 21104 - FindAll
给定一个模式字符串 P 和一个文本字符串 T,实现类型 FindAll<T,P>,该类型返回一个数组,其中包含 P 匹配的 T 中的所有索引(从 0 开始的索引)。
type FindAll<
T extends string,
P extends string,
Index extends any[] = [],
Res extends number[] = [],
> = P extends ""
? []
: T extends `${infer _F}${infer R}`
? T extends `${P}${infer _}`
? FindAll<R, P, [...Index, 0], [...Res, Index["length"]]>
: FindAll<R, P, [...Index, 0], Res>
: Res;
94. Medium - 21106 - Combination key type
- 把多个修饰键两两组合,但不可以出现相同的修饰键组合。
- 提供的
ModifierKeys中,前面的值比后面的值高,即cmd ctrl是可以的,但ctrl cmd是不允许的。
type Combs<T extends string[], U extends string = T[number]> =
T extends [infer F extends string, ...infer R extends string[]]
? U extends any
? F extends U
? never
: `${F} ${U}` | Combs<R>
: never
: never
95. Medium - 21220 - Permutations of Tuple
给定一个泛型元组类型 T extends unknown[],编写一个类型生成 T 的全排列作为一个并集。
type Join<U extends any[], F> = U extends any ? [F, ...U] : []
// [...T,...Pre]的全排列中以T中每个元素开头的部分
type PermutationsOfTuple<
T extends unknown[],
Pre extends any[] = []
> = T extends [infer F, ...infer R]
?
| Join<PermutationsOfTuple<[...R, ...Pre]>, F>
| (R['length'] extends 0 ? never : PermutationsOfTuple<R, [...Pre, F]>)
: []
这种题真恶心吐了,想了一下午。
96. Medium - 25170 - Replace First
实现类型 ReplaceFirst<T, S, R> 把元组 T 中第一个 S 替换为 R。
type ReplaceFirst<T extends readonly unknown[], S, R, Pre extends any[] = []> =
T extends [infer F, ...infer Rest]
? F extends S
? [...Pre, R, ...Rest]
: ReplaceFirst<Rest, S, R, [...Pre, F]>
: Pre
97. Medium - 25270 - Transpose ※
矩阵的转置是一种将矩阵翻转到其对角线上的运算符;也就是说,它通过产生另一个矩阵(通常用表示)来转换矩阵A的行和列索引。
// 把T中每一个子数组在下标Index对应的元素组成一个新数组
type PushAllByIndex<
T extends any[],
Index extends any[],
Res extends any[] = [],
> = T extends [infer F extends any[], ...infer R extends any[]]
? PushAllByIndex<R, Index, [...Res, F[Index["length"]]]>
: Res;
/**
* 翻转矩阵
* 把原数组中所有数组的第0个组成一个新数组,所有数组的第1个组成一个新数组,...
*/
type Transpose<
M extends number[][],
Res extends number[][] = [],
Index extends any[] = [],
> = Index['length'] extends M[0]["length"] // 遍历所有的index
? Res
: Transpose<M, [...Res, PushAllByIndex<M, Index>], [...Index, 0]>;
有点烧脑的,我加了一个辅助类型 PushAllByIndex。
在评论区看到一个精彩的解法 #25297
type Transpose<M extends number[][], R = M["length"] extends 0 ? [] : M[0]> = {
[X in keyof R]: {
[Y in keyof M]: X extends keyof M[Y] ? M[Y][X] : never
}
}
利用映射类型进行两层遍历,取 R 为原矩阵第一行,如果 M 为空数组,则为 []。然后先遍历 R 也是遍历原矩阵的每一列,再遍历 M 也就是每一行,再根据下标取逆转前对应的元素。
如果直接按下面这么写,会有类型报错,所以加了一层类型约束。
...{
[X in keyof R]: {
[Y in keyof M]: M[Y][X] // Type 'X' cannot be used to index type 'M[Y]'.
}
}
98. Medium - 26401 - JSON Schema to TypeScript
实现泛型类型 JSONSchema2TS,它返回与给定 JSON schema 对应的 TypeScript 类型。
type JSONSchema = {
type: string
enum?: any[]
properties?: Record<string, JSONSchema>
required?: string[]
items?: JSONSchema
}
type IntersectionToObj<T> = {
[K in keyof T]: T[K]
}
type RequiredByKeys<T, K extends keyof T = keyof T> = IntersectionToObj<
Omit<T, K> & Required<Pick<T, K>>
>
type JSONSchema2TS<T extends JSONSchema> =
T['enum'] extends any[] // 先判断是否为枚举类型
? T['enum'][number] // 如果是枚举类型直接返回对应枚举
: T['type'] extends 'string' // 判断是否为字符串类型
? string
: T['type'] extends 'number' // 判断是否为数字类型
? number
: T['type'] extends 'boolean' // 判断是否为布尔类型
? boolean
: T['type'] extends 'array' // 判断是否为数组类型
? T['items'] extends JSONSchema // 判断是否制定了数组元素类型
? JSONSchema2TS<T['items']>[] // 如果指定了子元素类型,先递归解析出子元素类型,然后构造数组
: unknown[] // 没有指定子类型,直接返回 unknown[]
: T['type'] extends 'object' // 判断是否为对象类型
? T['properties'] extends Record<string, JSONSchema> // 是否指定了对象属性
? T['required'] extends string[] // 是否指定了required属性
// 指定了required属性,利用之前的RequiredByKeys类型,把对应属性变成required
? RequiredByKeys<
{
[K in keyof T['properties']]?: JSONSchema2TS<
T['properties'][K]
>
},
T['required'][number]
>
: { // 没有指定required,所有属性都是可选的
[K in keyof T['properties']]?: JSONSchema2TS<
T['properties'][K]
>
}
: Record<string, unknown> // 没有指定属性,返回Record<string, unknown>
: never
枚举+递归,不难,但是分支有点多。利用了之前实现的 RequiredByKeys 工具类型。
99. Medium - 27133 - Square ※
给一个数字,返回它的平方。
目前为止,遇到的最恶心的一道题,我简直写了一套大数模拟算法。
/**
* 反转字符串
*/
type Reverse<T extends string | number> = `${T}` extends `${infer F}${infer R}`
? `${Reverse<R>}${F}`
: ''
/**
* 得到一个指定长度的元组
*/
type GetTuple<N> =
N extends '0' ? [] :
N extends '1' ? [0] :
N extends '2' ? [0, 0] :
N extends '3' ? [0, 0, 0] :
N extends '4' ? [0, 0, 0, 0] :
N extends '5' ? [0, 0, 0, 0, 0] :
N extends '6' ? [0, 0, 0, 0, 0, 0] :
N extends '7' ? [0, 0, 0, 0, 0, 0, 0] :
N extends '8' ? [0, 0, 0, 0, 0, 0, 0, 0] :
N extends '9' ? [0, 0, 0, 0, 0, 0, 0, 0, 0] : []
/**
* 计算个位数字A+B+C是否会进位(C=0/1)
*/
type GetAddCarry<A extends string, B extends string, C extends string = '0'> = [
...GetTuple<A>,
...GetTuple<B>,
...GetTuple<C>,
][9] extends 0
? '1'
: '0'
/**
* 计算个位数字A+B+C的个位数(C=0/1)
*/
type GetAddDigit<
A extends string,
B extends string,
C extends string = '0',
AddTuple extends number[] = [...GetTuple<A>, ...GetTuple<B>, ...GetTuple<C>],
> = AddTuple[9] extends 0
? `${AddTuple['length']}` extends `1${infer R}`
? R
: 0
: `${AddTuple['length']}`
/**
* 计算A+B(翻转后)
*/
type AddUtil<
A,
B,
Carry extends '0' | '1' = '0',
Result extends string = '',
> = A extends `${infer AFirst}${infer ARest}`
? B extends `${infer BFirst}${infer BRest}`
? AddUtil<
ARest,
BRest,
GetAddCarry<AFirst, BFirst, Carry>,
`${GetAddDigit<AFirst, BFirst, Carry>}${Result}`
>
: AddUtil<
ARest,
'',
GetAddCarry<AFirst, '0', Carry>,
`${GetAddDigit<AFirst, '0', Carry>}${Result}`
>
: B extends `${infer BFirst}${infer BRest}`
? AddUtil<
'',
BRest,
GetAddCarry<'0', BFirst, Carry>,
`${GetAddDigit<'0', BFirst, Carry>}${Result}`
>
: Carry extends '1'
? `1${Result}`
: Result
/**
* 计算 A+B
*/
type Add<A extends number, B extends number> = AddUtil<Reverse<A>, Reverse<B>>
/**
* 把字符串转成数字
*/
type String2Number<S> = S extends `${infer N extends number}` ? N : 0
// 计算 X*N
type Multi<
X extends number,
N extends number,
Index extends any[] = [],
Res extends number = 0,
> = Index['length'] extends N
? Res
: Multi<X, N, [...Index, 0], String2Number<Add<Res, X>>>
/**
* 取数字绝对值
*/
type Abs<N extends number> = `${N}` extends `-${infer Num extends number}` ? Num : N
/**
* 利用乘法计算平方
*/
type Square<N extends number> = Multi<Abs<N>, Abs<N>>
type Answer = Square<999> // 998001
100. Medium - 27152 - Triangular number
给定一个数字 N,找到第 N 个三角形数,即 1+2+3+...+N。
type Triangular<
N extends number, // 传入的参数N
Cur extends any[] = [], // 当前Index对应的答案
Index extends any[] = [] // 计算到第几个数字
> = Index['length'] extends N // 从0开始计算,是否计算到了N
? Cur['length'] // Cur的长度记录答案
: Triangular<N, [...Cur, ...Index, 0], [...Index, 0]>; // f(n+1)=f(n)+(n+1)
101. Medium - 27862 - CartesianProduct
给定两个集合(联合),返回其笛卡尔积的元组集合。分布式条件类型秒了。
type CartesianProduct<T, U> = T extends any
? U extends any
? [T, U]
: never
: never;
102. Medium - 27932 - MergeAll ※
将可变数量的类型合并到一个新类型中。如果键重叠,则其值应合并为一个联合。
type MergeAll<XS, Result extends Record<string, any> = {}> = XS extends [
infer F,
...infer R
]
? MergeAll<
R,
{
[K in keyof F | keyof Result]: K extends keyof F
? K extends keyof Result
? F[K] | Result[K]
: F[K]
: K extends keyof Result
? Result[K]
: never;
}
>
: Result;
最简单的方案,递归依次 Merge 每一个元素。
然后尝试一次出结果,我开始是这么写的,但是明显不对,U 是联合类型,在映射类型中会触发分布式。
type MergeAll<XS extends any[], U = XS[number]> = {
[K in keyof U]: U[K]
}
type exp = MergeAll<[{ a: string }, { b: string }]>
// MergeAll<[{
// a: string;
// }, {
// b: string;
// }], {
// a: string;
// }> | MergeAll<[{
// a: string;
// }, {
// b: string;
// }], {
// b: string;
// }>
所以需要提前拿到键的集合
type MergeAll<
XS extends any[],
U = XS[number],
Keys extends PropertyKey = U extends U ? keyof U : never
> = {
[K in Keys]: U extends U ? (K extends keyof U ? U[K] : never) : never;
};
其中 (K extends keyof U ? U[K] : never) 是为了限制 K 只访问对象实际存在的键。可以简化成下面的形式,U[K & keyof U]:
type MergeAll<
XS extends object[],
U = XS[number],
Keys extends PropertyKey = U extends U ? keyof U : never
> = {
[K in Keys]: U extends U ? U[K & keyof U] : never
}
103. Medium - 27958 - CheckRepeatedTuple
实现类型 CheckRepeatedChars<T> 返回元组类型 T 中是否有相同的成员。
type Includes<T extends unknown[], U> =
T extends [infer F, ...infer R]
? Equal<U, F> extends true
? true
: Includes<R, U>
: false
type CheckRepeatedTuple<T extends unknown[]> =
T extends [infer F, ...infer Rest]
? Includes<Rest, F> extends true
? true
: CheckRepeatedTuple<Rest>
: false
总感觉这题做过,放在这里没啥意思。本来想用联合类型作为一个 Set 来判断的,但是有这种测试用例: [boolean, true, false] 而 boolean 在分布式条件类型会被分布为 false 和 true,所以就两层循环判断了。
type TestUnion<U> = U extends U ? [U] : never
type R = TestUnion<boolean> // [false] | [true]
104. Medium - 28333 - Public Type
移除类型 T 中所有以 _ 开头的属性。
type PublicType<T extends object> = {
[K in keyof T as K extends `_${string}` ? never : K]: T[K]
}
涉及知识点:映射类型中使用 as 重命名键名和模板字符串类型。
105. Medium - 29650 - ExtractToObject
实现一个将指定属性 Prop 的值提取到接口的类型。该类型接受两个参数。输出应该是一个具有 Prop 值的对象。Prop 的值是对象。
type ExtractToObject<T, U extends keyof T> = {
[K in keyof T | keyof T[U] as K extends U ? never : K]:
K extends keyof T
? T[K]
: K extends keyof T[U]
? T[U][K]
: never
}
// 或者利用Omit
type ExtractToObject<T, U extends keyof T> = Omit<Omit<T, U> & T[U], never>
106. Medium - 29785 - Deep Omit
实现 DeepOmit, 类似于工具类型 Omit,可以删除多层子属性。
type DeepOmit<T, Props> =
Props extends `${infer L}.${infer R}`
? Omit<Omit<T, L> & Record<L, DeepOmit<T[L & keyof T], R>>, never>
: Omit<T, Props & keyof T>
107. Medium - 30301 - IsOdd
判断一个数字是否为奇数。注意到测试用例有 2.3 和 3e23,前者是小数,不可能是奇数,后者是大数,所以用到了 bigint 它可以匹配大整数,不支持小数。如果用 number 的话,它会匹配小数。
type IsOdd<T extends number> =
`${T}` extends `${bigint |''}${1 | 3 | 5 | 7 | 9}`
? true
: false;
108. Medium - 30430 - Tower of hanoi
模拟汉诺塔谜题的解决方案,将环的数量作为输入,并返回一系列步骤,将环从塔 A 移动到塔 B,并使用塔 C 作为辅助。数组中的每个条目都应该是一对字符串 [From,To],表示环被移动 From -> To。
// 处理 N-Index['length']
type Hanoi<
N extends number,
From = 'A',
To = 'B',
Intermediate = 'C',
Index extends number[] = []
> = Index['length'] extends N
? []
: [
...Hanoi<N, From, Intermediate, To, [...Index, 0]>,
[From, To],
...Hanoi<N, Intermediate, To, From, [...Index, 0]>
];
109. Medium - 30958 - Pascal's triangle
给一个数字,构造 Pascal's triangle(帕斯卡三角形、杨辉三角)。 Wikipedia
// 生成一个长度为N的元组,之前实现过复杂的,这里数据小,写个简单的
type GetNTuple<N, Index extends any[] = []> = Index['length'] extends N ? Index : GetNTuple<N, [...Index, 0]>
// 计算两数之和
type Add<A, B> = [...GetNTuple<A>, ...GetNTuple<B>]['length']
// 根据上一行生成下一行,每个数字是上一行当前位置的数字+前一个数字,最后再加一个1
type NextLine<Row extends any[], Pre = 0, Res extends any[] = []> =
Row extends [infer L, ...infer R]
? NextLine<R, L, [...Res, Add<L, Pre>]>
: [...Res, 1]
// 生成高度为N的三角 Index+1表示当前计算到的下标 计算到N-1的时候就可以返回结果了
type Pascal<N extends number, Index extends any[] = [], Res extends any[] = [[1]]> =
[...Index, 0]['length'] extends N // 计算到了所需要的高度,返回结果
? Res
: Pascal<N, [...Index, 0], [...Res, NextLine<Res[Index['length']]>]>
110. Medium - 30970 - IsFixedStringLiteralType ※
有时你可能想要判断一个字符串字面量是否是确定的类型。例如,当你想检查作为类标识符指定的类型是否是固定的字符串字面量类型时。(下面例子中,ID需要为一个确定的类型,string 约束太宽松)
type Action<ID extends string> = { readonly id: ID };
由于它必须是固定的,以下类型必须被判定为 false:
never类型- 字符串字面量类型的联合类型
- 包含嵌入 string、number、bigint、boolean 的模板字面量类型
请判断给定的类型 S 是否是一个确定的字符串字面量类型。
我洋洋洒洒写了几十行,结果看到大佬的答案,极其优雅 #33984
type IsFixedStringLiteralType<S extends string> =
{} extends Record<S, 1>
? false
: Equal<[S], S extends unknown ? [S] : never>
仔细分析一下 {} extends Record<S, 1> 所有情况:
-
如果
S是有限的键,在映射类型中,每个属性都是 required,此时{} extends Record<S, 1>为false。Record<`${boolean}`, 1> // { false: 1; true: 1; } Record<`${undefined}`, 1> // { undefined: 1; } Record<`ABC`, 1> // { ABC: 1; } -
如果
S是never那么Record<S, 1>就是{}。 -
如果
S是无限的集合,那就是索引签名,此时相当于每个键都是 optional 所以{}可以赋值给它。Record<`${number}`, 1> // [x: `${number}`]: 1; Record<`${string}`, 1> // [x: string]: 1;
太精彩了!
第二句 Equal<[S], S extends unknown ? [S] : never> 筛除了联合类型。
111. Medium - 34007 - Compare Array Length
实现 CompareArrayLength 以比较两个数组的长度。
若 T 数组长度大于 U,返回 1;若 U 数组长度大于 T,返回 -1;若 T 数组长度等于 U,返回 0。
type CompareArrayLength<
T extends any[],
U extends any[]
> = T['length'] extends U['length']
? 0
: keyof U extends keyof T ? 1 : -1;
112. Medium - 34857 - Defined Partial Record
生成 T 所有非空属性子集的联合类型。
type DefinedPartial<T, K extends keyof T = keyof T> = K extends any
? T | DefinedPartial<Omit<T, K>>
: never;
解析:举例对于 {a, b, c} 其所有非空子集是:{a,b,c}, {a,b}, {a,c}, {b,c}, {a}, {b}, {c}
而对于每个元素 K,可以选择"去掉它"或"保留它"
通过分布式条件类型来枚举子属性 K。
113. Medium - 35045 - Longest Common Prefix
实现类型 LongestCommonPrefix 返回元组的最长公共前缀,如果没有公共前缀,返回空字符串 ""。
type LongestCommonPrefix<
T extends string[],
P extends string = '',
U = T[number]
> = T[0] extends `${P}${infer N}${string}`
? false extends (U extends `${P}${N}${string}` ? true : false)
? P
: LongestCommonPrefix<T, `${P}${N}`>
: P;
P 作为 T[0] 的前缀,不断增加长度,利用分布式一次判断 P+下一个字符N 是否能匹配元组中所有字符串,如果可以就继续递归处理。否则返回当前最长公共前缀 P。
114. Medium - 35191 - Trace
方阵的迹(Trace)是指其主对角线元素的和。然而,使用类型系统难以计算该和。为简化处理,我们可将主对角线元素返回为联合类型。
type Trace<
T extends any[][],
Index extends any[] = []
> = Index['length'] extends T['length']
? never
: T[Index['length']][Index['length']] | Trace<T, [...Index, 0]>;
还有更天才的解法,通过映射类型来遍历下标生成一个对象,值为下标对应元素 T[Index][Index],然后再通过 [number] 取所有元素的联合。
type Trace<T extends any[][]> = {
[Index in keyof T]: T[Index][Index & keyof T[Index]];
}[number];
115. Medium - 35252 - IsAlphabet
判断给定的字符是否为字母。
type IsAlphabet<S extends string> = Uppercase<S> extends Lowercase<S> ? false : true;
利用了内置工具类型,太妙了。
Hard
终于做到了 Hard :D
116. Hard - 6 - Simple Vue ※
实现类似Vue的类型支持的简化版本。
type GetComputed<C> = C extends Record<string, (...args: any[]) => any>
? { [K in keyof C]: ReturnType<C[K]> }
: never;
declare function SimpleVue<D, C, M>(options: {
data: (this: void) => D;
computed: C & ThisType<D>;
methods: M & ThisType<D & GetComputed<C> & M>;
}): unknown;
这里学习了一个新知识 ThisType<T> :
在默认情况下,一个对象字面量内部函数的 this 指向的是这个对象本身。但在很多库(如 Vue、Pinia、JQuery)中,框架会通过 call 或 apply 动态改变 this 的指向。
ThisType<T> 就是为了解决这个“类型断层”而生的。它告诉编译器:“在这个对象字面量里,不管原本 this 该是谁,现在请把它看作 T。”
如果你定义一个普通对象,this 只能看到对象里已有的东西。
const obj = {
x: 10,
getX() {
return this.x; // 正常,this 指向 obj
},
getY() {
return this.y; // 报错:Property 'y' does not exist
}
}
可以手动指定 this 参数,注意 this 必须是第一个参数:
function Example(
this: { name: string }, // 伪参数,指定 this 的类型
age: number, // 真正的第 1 个参数
city: string // 真正的第 2 个参数
) {
console.log(`${this.name} is ${age} years old and lives in ${city}.`);
}
当你手动指定了 this,你不能直接像普通函数那样调用它(除非调用者的上下文正好符合要求),通常需要使用 .call()、.apply() 或 .bind()。
// 直接调用会报错,因为 TypeScript 认为没有提供符合要求的 this
// Example(25, "Beijing"); // Error
const user = { name: "Alice" };
// 正确调用方式:
Example.call(user, 25, "Beijing");
Example.apply(user, [25, "Beijing"]);
const boundExample = Example.bind(user);
boundExample(25, "Beijing");
在对象/类方法中使用:如果你在对象方法里手动指定 this,它会覆盖 TS 默认推导的类型。
interface DB {
filter(
this: string[], // 强制要求 this 必须是一个字符串数组
callback: (val: string) => boolean
): string[];
}
const myDb: DB = {
filter(callback) {
// 这里的 this 被限定为 string[]
return this.filter(callback);
}
};
// 错误用法:
// const wrong = { filter: myDb.filter, x: 1 };
// wrong.filter(s => s.length > 0); // 报错:The 'this' context... is not assignable to 'string[]'
通过 ThisType<T> 注入 this 的类型。
interface MyContext {
x: number;
log: (msg: string) => void;
}
// 通过 & ThisType<MyContext>,我们强行覆盖了 desc 内部的 this 类型
const desc: { data: string, setup: Function } & ThisType<MyContext> = {
data: "hello",
setup() {
this.x = 10; // 正确,MyContext.x: number
this.log("success"); // 正确,MyContext.log: (msg: string) => void
console.log(this.data); // 错误,Property 'data' does not exist on type 'MyContext'.
}
};
desc.data = 'data' // 正确 data: string
desc.x = 10 // 错误 Property 'x' does not exist
当使用 let obj: Type & ThisType<Context> 时:
- 成员检查:
obj外部依然遵循Type的约束(你不能给obj随便赋值Context里的属性)。 - 上下文推导:当编译器进入
obj内部的函数体时,它会跳过默认的推导逻辑,直接查阅ThisType提供的类型。
回到本题,根据 Vue 的定义,我们需要把 data,methods 和 computed 的值都放进 this 其中 computed 是函数,但是使用的时候是计算后的对象,所以需要取函数的返回值。
117. Hard - 17 - Currying 1 - 柯里化 1 ※
柯里化 是一种将带有多个参数的函数转换为每个带有一个参数的函数序列的技术。
type Curried<Fn> = Fn extends (...args: infer P) => infer R
? P['length'] extends 1 | 0 // 如果函数只有一个参数或者没有参数,不需要处理
? Fn
: P extends [infer F, ...infer O]
? (a: F) => Curried<(...args: O) => R>
: R
: Fn;
declare function Currying<Fn>(fn: Fn): Curried<Fn>;
每次提取第一个参数类型,然后用剩下的参数递归处理。
118. Hard - 55 - Union to Intersection 联合类型转化为交叉类型 ※
实现高级工具类型 UnionToIntersection<U> 把联合类型改为交叉类型。
type UnionToIntersection<U> = (
U extends any ? (args: U) => any : never
) extends (args: infer R) => any
? R
: never;
这里涉及到一个新的知识点 —— TypeScript 的逆变与协变:
在 TypeScript 中,函数的返回值是协变的,而函数的参数是逆变的。
- 协变 (Covariant) :如果
A是B的子集,那么() => A也是() => B的子集。 - 逆变 (Contravariant) :如果
A是B的子集,那么(arg: B) => void才是(arg: A) => void的子集(方向反过来了)。
当编译器在逆变位置(即函数参数位)遇到同一个类型变量 R 的多个候选类型时,为了保证类型安全,它会推导这些候选类型的交叉类型。
type UnionToFunction<U> = U extends any ? (args: U) => any : never;
// 把联合类型转换为函数的联合类型
// UnionToFunction<'foo' | 42 | true>;
// ((args: true) => any) | ((args: 42) => any) | ((args: "foo") => any)
type UnionToIntersection<U> = UnionToFunction<U> extends (args: infer P) => any
? P
: never;
// 根据TypeScript逆变,P需要符合前面所有的函数参数,需要是前面所有函数参数的交叉类型。
119. Hard - 57 - Get Required 获得必需的属性 ※
实现高级工具类型 GetRequired<T>,该类型保留所有必需的属性。
type GetRequired<T> = {
[K in keyof T as T[K] extends Required<T>[K] ? K : never]: T[K]
}
// 或者
type GetRequired<T> = {
[K in keyof T as (
{} extends Pick<T, K> ? never : K // 如果 {} 能分配给 Pick<T, K>,说明 K 是可选的
)]: T[K]
}
在 TypeScript 中 foo: undefined 和 foo?: undefined 是不一样的,前者属性必须存在,后者则可以不存在。但是我们没办法显式处理 ? 这种 flag 只能通过上面的方案来判断。
120. Hard - 59 - Get Optional 获得可选属性
实现高级工具类型 GetOptional<T>,该类型保留所有可选属性
和上面的题反过来即可。
type GetOptional<T> = {
[K in keyof T as {} extends Pick<T, K> ? K : never]: T[K]
}
121. Hard - 89 - Required Keys 必需的键
实现高级工具类型 RequiredKeys<T>,该类型返回 T 中所有必需属性的键组成的一个联合类型。
type RequiredKeys<T> = keyof GetRequired<T>
利用前面的 GetRequired
122. Hard - 90 - Optional Keys 可选类型的键
实现高级工具类型OptionalKeys<T>,该类型将 T 中所有可选属性的键合并为一个联合类型。
type OptionalKeys<T> = keyof GetOptional<T>
123. Hard - 112 - Capitalize Words
实现 CapitalizeWords<T>,它将字符串的每个单词的第一个字母转换为大写,其余部分保持原样。
type CapitalizeWords<
S extends string,
W extends string = ''
> = S extends `${infer L}${infer R}`
? Uppercase<L> extends Lowercase<L> // 当前字符是否为字母
? `${Capitalize<`${W}${L}`>}${CapitalizeWords<R, ''>}` // 不是字母,一个单词已经结束了,把前面的单词首字母大写,接下处理下一个单词
: CapitalizeWords<R, `${W}${L}`> // 是字母,这个字符和前面的字符连接起来
: Capitalize<W> // 字符串处理完了,把最后一个单词大写
很奇怪的一件事,我开始是这么写的,每个字符分别处理,Up 记录前一个字符是否不是字母,如果不是则下一个字符需要被大写:
type CapitalizeWords<
S extends string,
Up extends boolean = true
> = S extends `${infer L}${infer R}`
? Uppercase<L> extends Lowercase<L>
? `${L}${CapitalizeWords<R, true>}`
: Up extends true
? `${Uppercase<L>}${CapitalizeWords<R, false>}`
: `${L}${CapitalizeWords<R, false>}`
: S;
但是这个 case 出现了错误,我研究了很久,我的依赖版本是 "typescript": "^5.3.2",好像其他版本并没有问题。很奇怪。
CapitalizeWords<" Qq"> // `\uD83E\uDD23${any}${any}`
124. Hard - 114 - CamelCase
实现 CamelCase<T> ,将 snake_case 类型的表示的字符串转换为 camelCase 的表示方式。
type CamelCase<
S extends string,
Up extends boolean = false
> = S extends `${infer L}${infer R}` // 取S第一个字符L
? L extends '_' // 判断L是否为下划线
? R extends `${infer R1}${infer _}` // 取得下划线的下一个字符
? Lowercase<R1> extends Uppercase<R1> // 下划线后面接字母才处理,判断是否为字母
? `_${CamelCase<R, false>}` // 下一个字符不是字母,保留下划线
: CamelCase<R, true> // 下一个字符是字母,删除下划线,下一个字母大写
: '_' // 下划线后面没有其他字符了,保留下划线
: // 当前字符不是下划线,根据Up判断是否需要大写
`${Up extends true ? Uppercase<L> : Lowercase<L>}${CamelCase<R, false>}`
: '' // S为空串直接返回空
125. Hard - 147 - C-printf Parser
按照C语言的 printf 的格式来处理字符串中的 %d,%f 为元组 ['dec'], ['float'] 等。
type ParsePrintFormat<S> = S extends `${string}%${infer B}${infer C}`
? B extends keyof ControlsMap
? [ControlsMap[B], ...ParsePrintFormat<C>]
: ParsePrintFormat<C>
: []
126. Hard - 213 - Vue Basic Props ※
基于 6 - Simple Vue 实现带 props 的 Vue 类型。
type GetComputed<T extends Record<string, any>> = {
[K in keyof T]: ReturnType<T[K]>
}
type ToPrimitive<T> = T extends {
(): infer R
}
? R
: T extends RegExpConstructor
? RegExp
: T extends {
new (): infer A
}
? A
: T
type ToPrimitiveWithArray<T> = T extends any[]
? ToPrimitive<T[number]>
: ToPrimitive<T>
type GetProps<P> = {
[K in keyof P]: P[K] extends { type: infer F }
? ToPrimitiveWithArray<F>
: {} extends P[K]
? any
: ToPrimitiveWithArray<P[K]>
}
declare function VueBasicProps<
P,
D,
C extends Record<string, any>,
M
>(options: {
props: P
data: (this: GetProps<P>) => D
computed: C & ThisType<D>
methods: M & ThisType<D & GetComputed<C> & M & GetProps<P>>
}): any
虽然过程很艰难,但是我靠着不断尝试也算是写出来了。包括 T extends { (): infer R } 这种语法也是瞎试出来的。
下面是一些使用方法。
// 1. 函数类型(Function Type)
type F1 = () => R
// 2. 调用签名(Call Signature)—— 写在对象类型里
type F2 = { (): R }
// F1 和 F2 在大多数场景下完全等价
type IsEqual = F1 extends F2 ? true : false // true
// 对象类型中可以同时有:调用签名 + 属性 + 方法
type MyFunc = {
(): string // 调用签名:直接调用返回 string
(x: number): number // 重载调用签名
name: string // 普通属性
reset(): void // 方法
}
declare const fn: MyFunc
fn() // string
fn(42) // number
fn.name // string
fn.reset() // void
// 构造签名(new 关键字)
type Newable = { new(): string }
// 推断构造函数返回类型
type InstanceType<T> = T extends { new(): infer R } ? R : never
然后看了官方的解法 #215 还是很优雅,是正向解法,我写的有点像逆向解法,写的才会那么乱。
/**
* 推断 computed 对象的返回值类型
* 将 computed 中每个函数的返回值类型提取出来
* 例如: { count: () => number } → { count: number }
*/
type InferComputed<C extends Record<string, any>> = {
[K in keyof C]: ReturnType<C[K]>
}
/**
* Prop 的完整类型定义
* 支持两种写法:
* 1. 直接传构造函数/工厂函数: props: { name: String }
* 2. 传包含 type 字段的对象: props: { name: { type: String } }
*/
type Prop<T = any> = PropType<T> | { type?: PropType<T> }
/**
* PropType 支持单个构造器或构造器数组
* 例如: String 或 [String, Number]
*/
type PropType<T> = PropConstructor<T> | PropConstructor<T>[]
/**
* 构造器类型,支持两种形式:
* 1. { new (...args: any[]): T & object }
* 类的构造函数签名,T & object 确保:
* - new 只能返回对象类型(符合 JS 语义)
* - 与工厂函数分支互斥,避免推断歧义
* - 例如 String 构造函数:T & object = string & object = never,
* 因此 String 不会匹配此分支,而是走工厂函数分支
*
* 2. { (): T }
* 工厂函数签名(无参数),兜底处理原始类型
* 例如 String 作为工厂函数调用时返回 string,T = string
*/
type PropConstructor<T = any> =
| { new (...args: any[]): T & object }
| { (): T }
/**
* 从单个 Prop 定义中推断出其对应的 TS 类型
*
* 利用条件类型 + infer 提取泛型参数 T:
* - P extends Prop<infer T> 尝试从 P 中提取类型参数 T
* - 如果 P 是 {} 空对象,Prop<infer T> 中的 T 会被推断为 unknown
* 此时返回 any,避免过于严格的类型约束
* - 否则直接返回推断出的 T
*
* 例如:
* InferPropType<{ type: String }> → string
* InferPropType<{ type: Number }> → number
* InferPropType<{}> → any
*/
type InferPropType<P> = P extends Prop<infer T>
? unknown extends T
? any
: T
: any
/**
* 推断整个 props 对象的类型
* 遍历 props 定义对象,对每个 key 调用 InferPropType 提取类型
*
* 例如:
* InferProps<{ name: { type: StringConstructor }, age: { type: NumberConstructor } }>
* → { name: string, age: number }
*/
type InferProps<P extends Record<string, any>> = {
[K in keyof P]: InferPropType<P[K]>
}
/**
* 模拟 Vue 的 Options API 组件定义函数
*
* 泛型参数:
* @param P - props 的原始定义类型
* @param D - data 函数返回值类型
* @param C - computed 对象类型(值为函数)
* @param M - methods 对象类型
* @param Props - 由 P 推断出的 props 实际类型(默认由 InferProps<P> 计算)
* @param Computed - 由 C 推断出的 computed 实际类型(默认由 InferComputed<C> 计算)
*
* options 参数:
* - props: prop 定义对象
* - data: 返回组件数据,this 指向 Props(可访问 props)
* - computed: 计算属性对象,this 指向 Props & D & Computed & M(可访问所有上下文)
* - methods: 方法对象,this 指向 Props & D & Computed & M(可访问所有上下文)
*
* 返回值:
* Props & D & Computed & M 的交叉类型,即组件实例的完整类型
*/
declare function VueBasicProps<
P extends Record<string, any>,
D,
C extends Record<string, any>,
M,
Props = InferProps<P>,
Computed = InferComputed<C>
>(options: {
props?: P
// data 中 this 只能访问 props,此时 data/computed/methods 尚未初始化
data(this: Props): D
// computed 和 methods 的 this 可访问全部上下文(props + data + computed + methods)
computed: C & ThisType<Props & D & Computed & M>
methods: M & ThisType<Props & D & Computed & M>
}): Props & D & Computed & M
127. Hard - 223 - IsAny ※
判断一个类型是否是 any。这题解法很多的。T extends never ? true : false 其他类型只能得到 true 或者 false 只有 any 会得到 true | false。
type IsAny<T> = boolean extends (T extends never ? true : false) ? true : false
在评论区还看到一个优雅的解法:
type IsAny<T> = 0 extends (1 & T) ? true : false;
这里利用了 any 的特殊规则,任何类型和 any 取交叉类型,还是 any。
128. Hard - 270 - Typed Get
参考 lodash 的 get 函数,实现 TypeScript 的类型版本:给一个对象和 . 分隔的键,获取对应嵌套的值类型。
type Get<T, K extends string> = K extends keyof T
? T[K]
: K extends `${infer L}.${infer R}`
? L extends keyof T
? Get<T[L], R>
: never
: never
129. Hard - 300 - String to Number
实现 Number.parseInt 的类型版本,把字符串转成数字。
type ToNumber<S extends string> = S extends `${infer N extends number}`
? N
: never
130. Hard - 399 - Tuple Filter ※
实现一个类型 FilterOut<T, F>,用于从元组 T 中过滤掉指定类型 F 的元素。
type FilterOut<T extends any[], F> =
T extends [infer L, ...infer R ]
? [L] extends [F]
? FilterOut<R, F>
: [L, ...FilterOut<R, F>]
: []
131. Hard - 472 - Tuple to Enum Object ※
在这个问题中,你实现的类型应当将给定的字符串元组转成一个行为类似枚举的对象。此外,枚举的属性一般是 pascal-case 的。如果传递了第二个泛型参数,且值为 true,那么返回值应当是一个 number 字面量。
type Enum<
T extends readonly string[],
N extends boolean = false,
Index extends any[] = []
> = Readonly<
Omit<
T extends readonly [infer A extends string, ...infer B extends string[]]
? Record<Capitalize<A>, N extends true ? Index['length'] : A> &
Enum<B, N, [...Index, 0]>
: {},
never
>
>
这里有一点要注意,readonly 的数组也要 extends 后面也要加 readonly:
type t1 = readonly ['a'] extends readonly [infer A, ...infer B] ? 1 : 2 // 1
type t2 = readonly ['a'] extends [infer A, ...infer B] ? 1 : 2 // 2
132. Hard - 545 - printf
根据 printf 解析字符串的规则,把字符串类型转成函数类型。
type Map = {
d: number
s: string
}
type Format<T extends string> = T extends `${string}%${infer F}${infer R}`
? F extends keyof Map
? (arg: Map[F]) => Format<R>
: Format<R>
: string
133. Hard - 553 - Deep object to unique
TypeScript 是结构类型系统,只要两个对象的字段和值类型一样,TS 就认为它们是同一种类型,可以互相赋值。
但有时候我们希望某个函数只接受特定的对象,而不是任何"长得像"的对象。
实现一个类型工具 DeepObjectToUniq<T>,对传入的对象类型做如下处理:
- 保留所有键名(字符串键、数字键都要保留)
- 保留所有属性的值类型不变
- 对对象本身以及所有深层嵌套的子对象,都打上唯一标记,让它们和原来的类型"不完全相同"
- 转换前后的类型可以互相赋值(不能破坏兼容性)
- 同一对象中,不同路径下结构相同的子对象,转换后必须是不同的类型
这道题目比较难理解,为了让对象和之前不同,但是还可以相互赋值,可以添加任意一个可选的属性。
type Foo = { foo: 2; baz: Quz; bar: Quz }
type Bar = { foo: 2; baz: Quz; bar: Quz & { quzz?: 0 } }
同一对象中,不同路径下结构相同的子对象转换后还必须不同,比如转换后,Foo[baz] 和 Foo[bar] 不同,所以我们考虑把键也放在这个新增的可选属性中,保证唯一性。
然后不同对象,相同的属性也视为不同,比如转换后,Foo[baz] 和 Bar[baz] 不同,所以把整个对象也放在新增的可选属性中,保证唯一性。
所以代码为:
type DeepObjectToUniq<O extends object, N = O> = Omit<
{
[K in keyof O]: O[K] extends object ? DeepObjectToUniq<O[K], N | K> : O[K]
} & Partial<Record<symbol, N>>,
never
>
134. Hard - 651 - Length of String 2
计算一个字符串的长度,字符串最长可能有999个字符。
type LengthOfString<S, Ans extends 0[] = []> = S extends `${string}${string}${string}${string}${string}${string}${string}${string}${string}${string}${infer R}`
? LengthOfString<R, [...Ans, 0,0,0,0,0,0,0,0,0,0]>
: S extends `${string}${infer R}`
? LengthOfString<R, [...Ans, 0]>
: Ans['length']
135. Hard - 730 - Union to Tuple ※
把联合转成元组。这题真是想破脑袋也想不出来。
// https://github.com/type-challenges/type-challenges/issues/737
// 联合类型转交叉类型
type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (
x: infer U
) => any
? U
: never
// 取联合类型的最后一个元素:LastUnion<1 | 2 | 3> => 3
// 先构造函数类型的联合 (x: 1) => any | (x: 2) => any | (x: 3) => any
// 在转成交叉类型 ((x: 1) => any) & ((x: 2) => any) & ((x: 3) => any)
// 因为对重载函数做条件类型推断时,只使用最后一个重载
// 所以L为3
type LastUnion<T> = UnionToIntersection<
T extends any ? (x: T) => any : never
> extends (x: infer L) => any
? L
: never
// 联合类型转元组
type UnionToTuple<T, Last = LastUnion<T>> = [T] extends [never]
? []
: [...UnionToTuple<Exclude<T, Last>>, Last]
UnionToIntersection 是前面的题目,不多说。
重点是 LastUnion 每次取元组最后一个元素:根据 TypeScript 2.8 的规范,对重载函数做条件类型推断时,只使用最后一个重载:
declare function foo(x: string): number;
declare function foo(x: number): string;
declare function foo(x: boolean): boolean;
type T30 = ReturnType<typeof foo>; // boolean
接下来每次取元组最后一个元素即可,直到元组为 never。
136. Hard - 847 - String Join ※
实现一个连接字符串的函数的类型。
这个函数是返回一个函数,形如 () => () => xx,调用的时候是串行调用 join('-')('a', 'b', 'c')
一开始写成了上面那种错误的方式,这样写 P 读取不到第二个函数的入参类型,永远是 [],改成下面的方式就正确了。
// 错误
// declare function join<D extends string, P extends string[]>(delimiter: D):
// (...parts: P) => JoinPartsWithDelimiter<D, P>
// 正确
declare function join<D extends string>(delimiter: D):
<P extends string[]>(...parts: P) => JoinPartsWithDelimiter<D, P>
137. Hard - 956 - DeepPick
实现一个名为 DeepPick 的类型,它扩展了 Pick 实用类型。该类型接受两个参数。
type DeepPickSingle<O, P> = P extends `${infer K}.${infer R}`
? Record<K, DeepPickSingle<O[K & keyof O], R>>
: P extends keyof O
? Record<P, O[P]>
: unknown
type DeepPick<O, P> = (
P extends any ? (args: DeepPickSingle<O, P>) => any : never
) extends (args: infer P) => any
? P
: never
先实现一个支持单一属性的 DeepPick,可以根据属性路径 . 分割来读取嵌套类型。再根据前面学会的联合转交叉,把多个结果转成交叉类型。
138. Hard - 1290 - Pinia
实现 Pinia 库的类型版本。
参考之前实现 Vue 的知识点,这个没有特别难。
type getObjReturn<T> = {
[K in keyof T]: T[K] extends () => infer R ? R : unknown
}
declare function defineStore<S, G, A>(store: {
id: string
state: () => S
getters: G & ThisType<getObjReturn<G> & Readonly<S>>
actions: A & ThisType<Readonly<getObjReturn<G>> & A & S>
}): Omit<S & getObjReturn<G> & A, never>
139. Hard - 1383 - Camelize
实现 Camelize 类型: 将对象属性名从蛇形命名snake_case(下划线命名) 转换为小驼峰命名camelCase。
type CamelizeString<S> = S extends `${infer A}_${infer B}`
? `${A}${CamelizeString<Capitalize<B>>}`
: S
type Camelize<T> = T extends [infer F, ...infer R]
? [Camelize<F>, ...(Camelize<R> extends any[] ? Camelize<R> : [])]
: T extends object
? {
[K in keyof T as CamelizeString<K>]: Camelize<T[K]>
}
: T
140. Hard - 2059 - Drop String
删除字符串中的指定字符。
type GetStrUnion<S> = S extends `${infer A}${infer B}` ? A | GetStrUnion<B> : ''
type DropString<S, R, U = GetStrUnion<R>> =
S extends `${infer A}${infer B}`
? A extends U ? DropString<B, R> : `${A}${DropString<B, R>}`
: ''
141. Hard - 2822 - Split
实现 Split 的类型版本。
type Split<
S extends string,
SEP extends string = 'never'
> = S extends `${infer A}${SEP}${infer B}`
? [A, ...Split<B, SEP>]
: S extends SEP
? []
: string extends S
? string[]
: [S]
142. Hard - 2828 - ClassPublicKeys ※
实现一个泛型 ClassPublicKeys<T>,用于获取一个类的所有公开(public)属性键名。
type ClassPublicKeys<C> = keyof C
keyof 作用于类时,天然只返回 public 成员,因为 private/protected 成员在结构类型层面对外不可见。
143. Hard - 2857 - IsRequiredKey
实现泛型 IsRequiredKey<T, K> 返回 K 是否都是 T 中的 required 属性。
参考前面的 #57 Get Required 即可。
type IsRequiredKey<T, K extends keyof T> = false extends (
K extends any ? (T[K] extends Required<T>[K] ? true : false) : never
)
? false
: true
144. Hard - 2949 - ObjectFromEntries
实现类型版本的 Object.fromEntries
type ObjectFromEntries<T extends [string, any]> = {
[K in T[0]]: T extends any ? T[0] extends K ? T[1] : never : never
}
// 更聪明一点
type ObjectFromEntries<T extends [string, any]> = {
[K in T[0]]: T extends [K, any] ? T[1] : never
}
145. Hard - 4037 - IsPalindrome
实现类型 IsPalindrome<T> 检查一个字符串是否是回文串。
type Reverse<T extends string> = T extends `${infer L}${infer R}`
? `${Reverse<R>}${L}`
: ''
type IsPalindrome<T extends string | number> = Reverse<`${T}`> extends `${T}`
? true
: false
先把字符串逆转,再看逆转前后是否相等。