astak16 / blog-ts-challenges Goto Github PK
View Code? Open in Web Editor NEWts类型体操学习笔记
ts类型体操学习笔记
实现 Merge
类型,将两个类型合并为一个新的类型,如果两个类型中有相同的 key
,后一个类型的 key
覆盖前面一个类型的 key
import type { Equal, Expect } from "@type-challenges/utils";
type Foo = {
a: number;
b: string;
};
type Bar = {
b: number;
c: boolean;
};
type cases = [
Expect<
Equal<
Merge<Foo, Bar>,
{
a: number;
b: number;
c: boolean;
}
>
>
];
type Merge<T, U> = {
[K in keyof T | keyof U]: K extends keyof U
? U[K]
: K extends keyof T
? T[K]
: never;
};
type Merge<T, U> = {
[K in keyof (T & U)]: K extends keyof U
? U[K]
: K extends keyof T
? T[K]
: never;
};
type Merge<T, U> = Omit<Omit<T, keyof U> & U, never>;
题目链接:TupleToUnion
实现 TupleToUnion
,返回数组的所有项的联合类型。
import type { Equal, Expect } from "@type-challenges/utils";
type cases = [
Expect<Equal<TupleToUnion<[123, "456", true]>, 123 | "456" | true>>,
Expect<Equal<TupleToUnion<[123]>, 123>>
];
type TupleToUnion<T extends unknown[]> = T[number];
// 等价于
type TupleToUnion<T> = T extends unknown[] ? T[number] : never;
T[number]
得到数组所有成员的联合类型
type TupleToUnion<T> = T[number & keyof T];
// 等价于
type TupleToUnion<T> = T[Extract<keyof T, number>];
keyof T
, T
是数组 keyof T
的结果是索引的联合类型number & keyof T
,筛选出 keyof T
中的 number
类型Extract<keyof T, number>
= number & keyof T
type TupleToUnion<T> = T extends Array<infer R> ? R : never;
// 等价于
type TupleToUnion<T> = T extends (infer R)[] ? R : never;
将数组的中的内容声明为 R
type TupleToUnion<T> = T extends [infer first, ...infer rest]
? first | TupleToUnion<rest>
: never;
first
,剩余项声明为 rest
TupleToUnion
type TupleToUnion<T> = T[any];
any
可以作为索引类型
type TupleToUnion<T extends any[]> = keyof {
[K in T[number]]: K;
};
将数组变成对象,数组的项作为对象的 key
,使用 keyof
就能得到数组所有项的联合类型。
题目链接:AppendToObject,答题
实现 AppendToObject
,往对象中添加一个属性
import type { Equal, Expect } from "@type-challenges/utils";
type test1 = {
key: "cat";
value: "green";
};
type testExpect1 = {
key: "cat";
value: "green";
home: boolean;
};
type test2 = {
key: "dog" | undefined;
value: "white";
sun: true;
};
type testExpect2 = {
key: "dog" | undefined;
value: "white";
sun: true;
home: 1;
};
type test3 = {
key: "cow";
value: "yellow";
sun: false;
};
type testExpect3 = {
key: "cow";
value: "yellow";
sun: false;
isMotherRussia: false | undefined;
};
type cases = [
Expect<Equal<AppendToObject<test1, "home", boolean>, testExpect1>>,
Expect<Equal<AppendToObject<test2, "home", 1>, testExpect2>>,
Expect<
Equal<
AppendToObject<test3, "isMotherRussia", false | undefined>,
testExpect3
>
>
];
type AppendToObject<T, K extends keyof any, V> = {
[key in keyof T | K]: key extends keyof T ? T[key] : V;
};
将 T
中 key
和泛型 K
变成联合类型,然后就可以通过遍历 key
得到新对象
type IntersectionToObject<T> = { [K in keyof T]: T[K] };
type AppendToObject<T, K extends keyof any, V> = IntersectionToObject<
T & {
[key in K]: V;
}
>;
type IntersectionToObject<T> = { [K in keyof T]: T[K] };
type AppendToObject<T, K extends keyof any, V> = IntersectionToObject<
T & Record<K, V>
>;
方法二和方法三是相同的思路,把两个对象变成交叉对象,然后再将交叉对象转变成普通对象
题目链接:LookUp
实现 LookUp
,满足下面功能
import type { Equal, Expect } from '@type-challenges/utils'
interface Cat {
type: 'cat'
breeds: 'Abyssinian' | 'Shorthair' | 'Curl' | 'Bengal'
}
interface Dog {
type: 'dog'
breeds: 'Hound' | 'Brittany' | 'Bulldog' | 'Boxer'
color: 'brown' | 'white' | 'black'
}
type Animal = Cat | Dog
type cases = [
Expect<Equal<LookUp<Animal, 'dog'>, Dog>>,
Expect<Equal<LookUp<Animal, 'cat'>, Cat>>,
]
type LookUp<T, U extends string> = {
[K in U]: T extends { type: U } ? T : never;
}[U];
T extends { type: U }
会利用分配率
Dog extends { type: "dog" }
=> true
=> Dog
Cat extends { type: "cat" }
=> false
=> never
得到 Dog | never
的联合类型,由于 never
是所有类型的子类型,所以最后得到 Dog
。
type LookUp<T, U extends string> = T extends { type: U } ? T : never;
简化了方法一,不需要通过对象取值
type LookUp<T, U extends string> = T extends (U extends T[keyof T] ? T : never)
? T
: never;
方法二中 { type: U }
,可以写成 U extends T[keyof T] ? T : never
type LookUp<T extends { type: any }, U extends string> = T extends unknown
? T["type"] extends U
? T
: never
: never;
这里 T["type"] extends U
不会运用分配率,会一直输入 never
。
为了能够让分配率生效,需要一个前置条件 T extends unknown
。
题目链接:StringToUnion,答题
实现 StringToUnion
,接收字符串作为参数,输出该字符串字母的联合类型
import type { Equal, Expect } from "@type-challenges/utils";
type cases = [
Expect<Equal<StringToUnion<"">, never>>,
Expect<Equal<StringToUnion<"t">, "t">>,
Expect<Equal<StringToUnion<"hello">, "h" | "e" | "l" | "l" | "o">>,
Expect<
Equal<
StringToUnion<"coronavirus">,
"c" | "o" | "r" | "o" | "n" | "a" | "v" | "i" | "r" | "u" | "s"
>
>
];
将字符串转换成字符的联合类型,有两种方法:
|
把所有字符连接起来Tuple[number]
把元组变成联合类型type StringToUnion<T extends string> = T extends `${infer F}${infer Rest}`
? F | StringToUnion<Rest>
: never;
type StringToUnion<
T extends string,
U extends unknown[] = []
> = T extends `${infer F}${infer Rest}`
? StringToUnion<Rest, [F, ...U]>
: U[number];
实现 Diff
,挑选出两个对象各自独有的属性
import type { Equal, Expect } from "@type-challenges/utils";
type Foo = {
name: string;
age: string;
};
type Bar = {
name: string;
age: string;
gender: number;
};
type Coo = {
name: string;
gender: number;
};
type cases = [
Expect<Equal<Diff<Foo, Bar>, { gender: number }>>,
Expect<Equal<Diff<Bar, Foo>, { gender: number }>>,
Expect<Equal<Diff<Foo, Coo>, { age: string; gender: number }>>,
Expect<Equal<Diff<Coo, Foo>, { age: string; gender: number }>>
];
type Diff<T, U> = Omit<T & U, keyof T & keyof U>;
// 或者这样写
type Diff<T, U> = Omit<T & U, keyof (T | U)>;
type Foo = {
name: string;
age: string;
};
type Bar = {
name: string;
age: string;
gender: number;
};
type a = keyof Foo & keyof Bar // => "name" | "age"
type b = keyof (Foo | Bar) // => "name" | "age"
type Diff<T, U> = {
[key in Exclude<keyof (T & U), keyof (T | U)>]: key extends keyof T
? T[key]
: key extends keyof U
? U[key]
: never;
};
// 或者这样写
type Diff<T, U> = {
[key in Exclude<keyof (T & U), keyof (T | U)>]: (T & U)[key];
};
type Foo = {
name: string;
age: string;
};
type Bar = {
name: string;
age: string;
gender: number;
};
type a = keyof (Foo & Bar); // "name" | "age" | "gender"
type aa = keyof Foo | keyof Bar; // "name" | "age" | "gender"
type b = keyof (Foo | Bar); // "name" | "age"
type bb = keyof Foo & keyof Bar; // "name" | "age"
type c = Exclude<a, b>; // "gender"
Exclude<...>
有很多种写法:
Exclude<keyof (T & U), keyof T & keyof U>
Exclude<keyof T | keyof U, keyof T & keyof U>
Exclude<keyof T, keyof U> | Exclude<keyof U, keyof T>
type Diff<T, U> = Required<
{
[K in Exclude<keyof T, keyof U>]: T[K];
} & {
[K in Exclude<keyof U, keyof T>]: U[K];
}
>;
// 或者用之前的 Merge 方法
type Diff<T, U> = Merge<
{
[K in Exclude<keyof T, keyof U>]: T[K];
},
{
[K in Exclude<keyof U, keyof T>]: U[K];
}
>;
T
中排除掉 U
生成一个新的对象U
中排除掉 T
生成一个新的对象type Diff<T, U> = Pick<
T & U,
Exclude<keyof T, keyof U> | Exclude<keyof U, keyof T>
>;
// 或者这样写
type Diff<
T,
U,
T1 extends keyof T = keyof T,
U1 extends keyof U = keyof U
> = Pick<
T & U,
| { [K in T1]: K extends U1 ? never : K }[T1]
| { [K in U1]: K extends T1 ? never : K }[U1]
>;
和方法三思路一样
type Diff<
T,
U,
T1 extends keyof T = keyof T,
U1 extends keyof U = keyof U,
I extends T & U = T & U
> = T extends U
? Omit<T, keyof Pick<T, U1>>
: U extends T
? Omit<U, keyof Pick<U, T1>>
: Required<Omit<T & U, keyof Pick<I, U1>> & Omit<I, keyof Pick<I, T1>>>;
这是我写的第一个方法,因为一开始没有思路,就想着分三步走
T extends U
挑选出 T
中独有的属性U extends T
挑选出 U
中独有的属性T
和 U
没有关系,挑选出 T
和 U
各自独有的属性题目链接:MyExclude
实现一个 MyExclude
满足测试用例 cases
a
"a" | "b"
Function
import { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<MyExclude<"a" | "b" | "c", "a">, Exclude<"a" | "b" | "c", "a">>>,
Expect<
Equal<
MyExclude<"a" | "b" | "c", "a" | "b">,
Exclude<"a" | "b" | "c", "a" | "b">
>
>,
Expect<
Equal<
MyExclude<string | number | (() => void), Function>,
Exclude<string | number | (() => void), Function>
>
>
];
type MyExclude<UnionType, ExcludedMembers> =
UnionType extends ExcludedMembers ? never : UnionType;
利用分配率,代入运算
type A1 = MyExclude<"a" | "b" | "c", "a">;
/** 代入过程 **/
// ==>
type A1 = MyExclude<"a", "a"> | MyExclude<"b", "a"> | MyExclude<"c", "a">;
// ==>
MyExclude<"a", "a"> // never
MyExclude<"b", "a"> // "b"
MyExclude<"c", "a"> // "c"
// ==>
type A1 = "b" | "c"
这里为什么没有这样定义:type MyExclude<UnionType, ExcludedMembers extends UnionType>
,因为如果对 ExcludeMembers
做了限制, () ⇒ void
和 Function
就必须匹配,否则无法剔除
type MyExclude<T, U, K extends T = T> = K extends U ? never : K;
K extends T = T
这个语法还不太懂
题目链接:DeepReadonly
实现类型 DeepReadonly
,将对象的每个子对象也变成只读。
import type { Equal, Expect } from "@type-challenges/utils";
type cases = [Expect<Equal<DeepReadonly<X>, Expected>>];
type X = {
a: () => 22;
b: string;
aa: {};
c: {
d: boolean;
e: {
g: {
h: { i: true; j: "string" };
k: "hello";
};
l: ["hi", { m: ["hey"] }];
};
};
};
type Expected = {
readonly a: () => 22;
readonly b: string;
readonly aa: {};
readonly c: {
readonly d: boolean;
readonly e: {
readonly g: {
readonly h: {
readonly i: true;
readonly j: "string";
};
readonly k: "hello";
};
readonly l: readonly ["hi", { readonly m: readonly ["hey"] }];
};
};
};
如果是对象,需要递归调用,但函数不需要
type DeepReadonly<T> = T extends object & { call?: never }
? { readonly [key in keyof T]: DeepReadonly<T[key]> }
: T;
{call?: never}
是用来排除函数的
type DeepReadonly<T> = {
readonly [key in keyof T]: T[key] extends Function
? T[key]
: T[key] extends object
? DeepReadonly<T[key]>
: T[key];
};
通过 T[key] extends Function
判断是不是函数,不是函数的话在判断是不是 object
type DeepReadonly<T> = {
readonly [key in keyof T]: keyof T[key] extends never
? T[key]
: DeepReadonly<T[key]>;
};
// 等价于
type DeepReadonly<T> = {
readonly [key in keyof T]: keyof T[key] extends object
? T[key]
: DeepReadonly<T[key]>;
};
// 等价于
type DeepReadonly<T> = keyof T extends never
? T
: { readonly [key in keyof T]: DeepReadonly<T[key]> };
// 等价于
type DeepReadonly<T> = keyof T extends object
? T
: { readonly [key in keyof T]: DeepReadonly<T[key]> };
keyof T[key] extends never
,如果 T[key]
为函数或者空对象, keyof T[key]
结果为 never
keyof () => void
⇒ never
keyof {}
⇒ never
同时 never extends object
⇒ true
这样就把函数和空对象排除掉了
type DeepReadonly<T> = T extends Record<string, unknown> | Array<unknown>
? { readonly [key in keyof T]: DeepReadonly<T[key]> }
: T;
const a: Record<string, unknown> = () => 1; // 报错
const c: Record<string, any> = () => 1; // 不报错
所以 Record<string, unknown>
不能用 any
代替 unknown
题目链接:Tuple to Enum Object,回答链接
实现 Enum
,满足下面要求
import type { Equal, Expect } from "@type-challenges/utils";
const OperatingSystem = ["macOS", "Windows", "Linux"] as const;
const Command = [
"echo",
"grep",
"sed",
"awk",
"cut",
"uniq",
"head",
"tail",
"xargs",
"shift",
] as const;
type cases = [
Expect<Equal<Enum<[]>, {}>>,
Expect<
Equal<
Enum<typeof OperatingSystem>,
{
readonly MacOS: "macOS";
readonly Windows: "Windows";
readonly Linux: "Linux";
}
>
>,
Expect<
Equal<
Enum<typeof OperatingSystem, true>,
{
readonly MacOS: 0;
readonly Windows: 1;
readonly Linux: 2;
}
>
>,
Expect<
Equal<
Enum<typeof Command>,
{
readonly Echo: "echo";
readonly Grep: "grep";
readonly Sed: "sed";
readonly Awk: "awk";
readonly Cut: "cut";
readonly Uniq: "uniq";
readonly Head: "head";
readonly Tail: "tail";
readonly Xargs: "xargs";
readonly Shift: "shift";
}
>
>,
Expect<
Equal<
Enum<typeof Command, true>,
{
readonly Echo: 0;
readonly Grep: 1;
readonly Sed: 2;
readonly Awk: 3;
readonly Cut: 4;
readonly Uniq: 5;
readonly Head: 6;
readonly Tail: 7;
readonly Xargs: 8;
readonly Shift: 9;
}
>
>
];
由题意可知 Enum
接收两个参数
string
数组boolean
,可选的,默认 false
如果第二个参数没有传递,则输出 key
和 value
都一样的对象
如果第二参数传了 true
,则 value
为 key
在数组中的索引
type EnsureArray<T, Type = string> = T extends Type[] ? T : never;
type Enum<
T extends readonly string[],
B extends boolean = false,
R extends Readonly<Record<string, any>> = Readonly<{}>,
Index extends unknown[] = []
> = T extends readonly [infer F, ...infer Rest]
? Enum<
Readonly<EnsureArray<Rest>>,
B,
Readonly<
R & {
[K in F & string as Capitalize<K>]: B extends true
? Index["length"]
: K;
}
>,
[...Index, unknown]
>
: R;
第一种方法是递归,把数组的拆成一项一项的,递归调用,生成最终的对象
所以实际的对象会接收 4
个参数,除了第一个参数是必传的,其余的参数都有默认值
string
数组,必传false
{}
[]
,因为空数组的 length
为 0
取出数组的第一项
R
,R
默认的是空对象T extends [infer F, ...infer Rest] ? ... : R
如果不是空数组,会进入 true
分支
true
分支就是递归部分第一个参数和第二个参数好理解,Rest
数组中剩余的参数,用不用 Readonly
包装都一样,第二个参数就是 boolean
值
主要看第三个参数和第四个参数
第三个参数默认是空对象,但是在递归调用的时候需要把数组项处理成对象
R & {...}
中的 R
第一次是空对象,第二次是第一次 {...}
处理的结果 {readonly MacOS: "macOS"}
{...}
把数组一项变成对象
F & string
是类型约束Capitalize
把首字母变为大写true
则输出 Index["length"]
(第一次为 0
),否则输出为 K
(第一次为 masOS
)Readonly
进行包装的话,输出的结果会是 {readonly MasOS: "masOS"} & {...} & {...}
这样的形式,而实际的输出为: {readonly MasOS: "masOS", ... }
,就需要使用 Readonly
包装,它可以把多个交叉类型的对象合并为一个。Readonly<R & {
[K in F & string as Capitalize<K>]: B extends true
? Index["length"]
: K
}>,
第四个参数是个数组,用来计算数组的索引
Index["length"]
,第一项为 0
,第一项运行结束是变为 [unknown]
Index["length"]
,第二项为 2
,第二项运行结束是变为 [unknown, unknown]
交叉类型可以做类型约束: F & string
把交叉类型合并为一个类型: Readonly<{a: string} & {b: string}>
变为 {readonly a: string; readonly b: string}
, Required
也可以
递归中获取索引:
Arr = []
Arr["length"]
[...Arr, unknown]
type EnsureArray<T, Type = string> = T extends Type[] ? T : never;
type Enum<T extends readonly string[], B extends boolean = false> = {
readonly [key in T[number] as Capitalize<key>]: B extends true
? FindIndex<T, key>
: key;
};
type FindIndex<
T extends readonly string[],
K extends string,
Index extends unknown[] = []
> = T extends readonly [infer F, ...infer Rest]
? F extends K
? Index["length"]
: FindIndex<EnsureArray<Rest>, K, [...Index, unknown]>
: never;
// FindIndex 另一种写法
type FindIndex<
T extends readonly string[],
E extends string,
Index extends unknown[] = []
> = T extends readonly [infer L, ...infer Rest]
? [E, L] extends [L, E]
? Index["length"]
: FindIndex<EnsureArray<Rest>, E, [...Index, unknown]>
: never;
// FindIndex 另一种写法
type FindIndex<
T extends readonly string[],
K extends string,
Index extends unknown[] = []
> = T[Index["length"]] extends K
? Index["length"]
: FindIndex<T, K, [...Index, unknown]>;
和方法一的区别是递归只用来获取索引
Enum
接收两个参数:
string
数组,必传false
遍历,将数组变为对象(如果第二个参数为 false
)
type Enum<T extends readonly string[]> = {
readonly [key in T[number] as Capitalize<key>]: key;
};
如果第二个参数为 true
,就要计算索引了,计算索引的方法和方法一相同
FindIndex
接收三个参数:
Enum
的第一个参数[]
,因为空数组的 length
为 0
这里面取数组的某一项有两种方法:
T[Index["length"]]
T extends [infer F, ...infer Rest]
递归调用就能取到索引
T[Index["length"]]
T extends [infer F, ...infer Rest]
[E, L] extends [L, E]
[1, 2] extends [2, 1]
⇒ false
就会递归type Enum<T extends readonly string[], B extends boolean = false> = {
[key in keyof T as T[key] extends string
? Capitalize<T[key]>
: never]: B extends false ? T[key & string] : StringToNumber<key & string>;
};
type StringToNumber<
S extends string,
R extends unknown[] = []
> = S extends `${R["length"]}`
? R["length"]
: StringToNumber<S, [...R, unknown]>;
这里看不太明白的应该是这一段
S extends `${R["length"]}` ? R["length"] : StringToNumber<S, [...R, unknown]>
这里我的理解,输出的应该都是 R["length"]
,因为传过来的就是字符串。
为了弄明白这一点,我把数字代进去,看结果,首先是 S = "0"
的情况,输出 0
没有问题
// S = "0"
// R["length"] = 0
// 输出 0
"0" extends `0` ? 0 : StringToNumber<S, [...R, unknown]>
看 S = "1"
的情况,就会循环递归 StringToNumber
// S = "1"
// R["length"] = 0
// 输出 StringToNumber<S, [...R, unknown]>
"1" extends `0` ? 0 : StringToNumber<S, [...R, unknown]>
// S = "1"
// R["length"] = 1
// 1
"1" extends `1` ? 1 : StringToNumber<S, [...R, unknown]>
为什么 T[number]
可以把元组变成联合类型,因为元组有一个 number
索引
type Arr = ["a", "b", "c"];
type A = keyof Arr; // number | "0" | "1" | "2" ...
type B = Arr[number]; // "a" | "b" | "c"
递归应用
// 字符串转数字
type StringToNumber<
S extends string,
R extends unknown[] = []
> = S extends `${R["length"]}`
? R["length"]
: StringToNumber<S, [...R, unknown]>;
type N = StringToNumber<"2">; // 2
// 寻找元组下标
type FindIndex<
T extends readonly string[],
K extends string,
Index extends unknown[] = []
> = T[Index["length"]] extends K
? Index["length"]
: FindIndex<T, K, [...Index, unknown]>;
type tuple = ["a", "b", "c", "d"];
type I = FindIndex<tuple, "c">; // 2
type TupleIndex<T extends readonly unknown[]> = T extends readonly [
...infer Rest,
infer L
]
? TupleIndex<Rest> & { [p in L & string]: Rest["length"] }
: {};
type Enum<T extends readonly string[], N extends boolean = false> = {
readonly [K in keyof TupleIndex<T> as Capitalize<K & string>]: N extends true
? TupleIndex<T>[K]
: K;
};
type tuple = ["macOS", "Windows", "Linux"];
type A = keyof TupleIndex<tuple> extends tuple[number] ? true : false; // true
可能你会觉得 keyof TupleIndex<tuple>
和 tuple[number]
是相等的,就会认为上面 keyof TupleIndex<T>
可以替换成 T[number]
type B<T extends string[]> = {
[K in keyof TupleIndex<T>]: K;
};
type C<T extends string[]> = {
[K in T[number]]: K;
};
type D = B<tuple> extends C<tuple> ? true : false; // true
但这不能简单的替换,需要做约束
type B<T extends string[]> = {
[K in keyof TupleIndex<T>]: TupleIndex<T>[K];
};
type C<T extends string[]> = {
[K in T[number]]: TupleIndex<T>[K]; // error
};
// ==>
type C<T extends string[]> = {
[K in T[number]]: TupleIndex<T>[K **& keyof TupleIndex<T>**];
};
为什么这里需要做约束呢?
因为 T extends string[]
中 T
是 string[]
,所以 K
是 string
但 TupleIndex<T>
的 T
不也是 string[]
么,为什么用 string
访问就会出错呢?
因为经过 TupleIndex<T>
会进行类型推导,会是 T
中的具体类型
也就是说 TupleIndex<T>
最后的类型比 string
更具体
所以直接用 string
去访问就会报错,需要进行约束
type Format<
T extends readonly unknown[],
P extends unknown[] = []
> = T extends readonly [infer F, ...infer R]
? [[F, P["length"]], ...Format<R, [...P, unknown]>]
: [];
type Enum<T extends readonly string[], B extends boolean = false> = {
readonly [K in Format<T>[number] as Capitalize<K[0]>]: B extends true
? K[1]
: K[0];
};
Format
实现的结果是双重数组: [["masOS", 0], ["Windows", 1], ["Linus", 2]]
type TupleIndex<T extends readonly unknown[]> = T extends readonly [
infer F,
...infer R
]
? TupleIndex<R> | R["length"]
: never;
type Enum<T extends readonly string[], B extends boolean = false> = {
readonly [K in TupleIndex<T> as Capitalize<T[K]>]: B extends true ? K : T[K];
};
TupleIndex
实现的结果是: TupleIndex<["macOS", "Windows", "Linux"]> // 0 | 2 | 1
A
类型和 B
类型写法上的区别
A
类型是将所有 Key
的首字母都变为大写,然后再将 tuple
中首字母是小写的那一项的 Key
的首字母变为小写
B
类型是应用 as
引导一个操作条件 Key
的值没有变
const tuple = ["macOS", "Windows", "Linux"] as const;
type A<T extends readonly string[]> = {
[Key in Capitalize<T[number]>]: Key extends T[number]
? Key
: Uncapitalize<Key>;
};
type B<T extends readonly string[]> = {
[Key in T[number] as Capitalize<Key & string>]: Key;
};
type a = A<typeof tuple>;
type b = B<typeof tuple>;
type c = a extends b ? true : false; // true
typeof
的用法
typeof
是用来判断类型的, type b
的结果为什么是 string[]
,因为 a
的类型是 string[]
, type c
的类型也就好理解了,因为 tuple
是常量数组。
const a = ["macOS", "Windows", "Linux"];
type b = typeof a; // string[]
const tuple = ["macOS", "Windows", "Linux"] as const;
type c = typeof tuple; // readonly ["macOS", "Windows", "Linux"]
确定类型
交叉类型可以做类型约束: F & string
=> string
EnsureType<T, C> = T & C;
EnsureType<T, C> = T extends C ? T : never;
type a = EnsureType<"1", string>; // "1";
type b = EnsureType<1, string>; // never;
把交叉类型合并为一个类型: Readonly<{a: string} & {b: string}>
变为 {readonly a: string; readonly b: string}
, Required
也可以
获取元组中某一项有方法:
T[Index["length"]]
T extends [infer F, ...infer Rest]
[E, L] extends [L, E]
[1, 2] extends [2, 1]
⇒ false
就会递归为什么 T[number]
可以把元组变成联合类型,因为元组有一个 number
索引
type Arr = ["a", "b", "c"];
type A = keyof Arr; // number | "0" | "1" | "2" ...
type B = Arr[number]; // "a" | "b" | "c"
递归应用
Arr = []
Arr["length"]
[...Arr, unknown]
// 字符串转数字
type StringToNumber<
S extends string,
R extends unknown[] = []
> = S extends `${R["length"]}`
? R["length"]
: StringToNumber<S, [...R, unknown]>;
type N = StringToNumber<"2">; // 2
// 寻找元组下标
type FindIndex<
T extends readonly string[],
K extends string,
Index extends unknown[] = []
> = T[Index["length"]] extends K
? Index["length"]
: FindIndex<T, K, [...Index, unknown]>;
type tuple = ["a", "b", "c", "d"];
type I = FindIndex<tuple, "c">; // 2
实现 KebabCase
,满足下面需求
import type { Equal, Expect } from "@type-challenges/utils";
type cases = [
Expect<Equal<KebabCase<"FooBarBaz">, "foo-bar-baz">>,
Expect<Equal<KebabCase<"fooBarBaz">, "foo-bar-baz">>,
Expect<Equal<KebabCase<"foo-bar">, "foo-bar">>,
Expect<Equal<KebabCase<"foo_bar">, "foo_bar">>,
Expect<Equal<KebabCase<"Foo-Bar">, "foo--bar">>,
Expect<Equal<KebabCase<"ABC">, "a-b-c">>,
Expect<Equal<KebabCase<"-">, "-">>,
Expect<Equal<KebabCase<"">, "">>,
Expect<Equal<KebabCase<"😎">, "😎">>
];
type KebabCase<T> = T extends `${infer FirstLetter}${infer RestLetter}`
? Rest extends Uncapitalize<Rest>
? `${Uncapitalize<FirstLetter>}${KebabCase<RestLetter>}`
: `${Uncapitalize<FirstLetter>}-${KebabCase<RestLetter>}`
: T;
FirstLetter
和 RestLetter
FirstLetter
部分,而是判断 RestLetter
部分首字母是否大写-
FirstLetter
,那单词首字母大写的话,就会在单词前面添加 -
不符合题意实现 Pop
,满足下面功能
import type { Equal, Expect } from "@type-challenges/utils";
type cases = [
Expect<Equal<Pop<[3, 2, 1]>, [3, 2]>>,
Expect<Equal<Pop<["a", "b", "c", "d"]>, ["a", "b", "c"]>>
];
type Pop<T extends unknown[]> = T extends [...infer R, infer L] ? R : [];
type Pop<T extends unknown[]> = T extends [infer F, ...infer R]
? R extends []
? []
: [F, ...Pop<R>]
: [];
type Pop<T extends unknown[]> = T extends [infer F, ...infer R]
? T["length"] extends 0 | 1
? []
: [F, ...Pop<R>]
: [];
type RemoveFirst<T extends any[]> = T extends [infer F, ...infer R] ? R : [];
type Pop<T extends unknown[]> = T["length"] extends 0 | 1
? []
: [T[0], ...Pop<RemoveFirst<T>>];
type Shift<T extends unknown[]> = T extends [infer F, ...infer R] ? R : [];
type Shift<T extends unknown[]> = T extends [...infer R, infer L]
? R extends []
? []
: [...Shift<R>, L]
: [];
type Shift<T extends unknown[]> = T extends [...infer R, infer L]
? T["length"] extends 0 | 1
? []
: [...Shift<R>, L]
: [];
type RemoveLast<T extends unknown[]> = T extends [...infer R, infer L] ? R : [];
type Shift<T extends unknown[]> = T["length"] extends 0 | 1
? []
: [...Shift<RemoveLast<T>>, [unknown, ...T][T["length"]]];
type Unshift<T extends unknown[], V> = [V, ...T];
type Push<T extends unknown[], V> = [...T, V];
type Reverse<T extends unknown[], Target extends unknown[] = []> = T extends [
infer F,
...infer R
]
? Reverse<R, [F, ...Target]>
: Target;
题目链接:UnionToTuple
实现 UnionToTuple
,将联合类型转为元组。
import type { Equal, Expect } from "@type-challenges/utils";
type ExtractValuesOfTuple<T extends any[]> = T[keyof T & number];
type cases = [
Expect<Equal<UnionToTuple<"a" | "b">["length"], 2>>,
Expect<Equal<ExtractValuesOfTuple<UnionToTuple<"a" | "b">>, "a" | "b">>,
Expect<Equal<ExtractValuesOfTuple<UnionToTuple<"a">>, "a">>,
Expect<Equal<ExtractValuesOfTuple<UnionToTuple<any>>, any>>,
Expect<
Equal<ExtractValuesOfTuple<UnionToTuple<undefined | void | 1>>, void | 1>
>,
Expect<Equal<ExtractValuesOfTuple<UnionToTuple<any | 1>>, any | 1>>,
Expect<Equal<ExtractValuesOfTuple<UnionToTuple<any | 1>>, any>>,
Expect<
Equal<
ExtractValuesOfTuple<UnionToTuple<"d" | "f" | 1 | never>>,
"f" | "d" | 1
>
>,
Expect<
Equal<ExtractValuesOfTuple<UnionToTuple<[{ a: 1 }] | 1>>, [{ a: 1 }] | 1>
>,
Expect<Equal<ExtractValuesOfTuple<UnionToTuple<never>>, never>>,
Expect<
Equal<
ExtractValuesOfTuple<
UnionToTuple<"a" | "b" | "c" | 1 | 2 | "d" | "e" | "f" | "g">
>,
"f" | "e" | 1 | 2 | "g" | "c" | "d" | "a" | "b"
>
>
];
这题有两个核心点:
联合类型转为函数交叉类型
type Union = 1 | 2;
type A = (() => 1) & (() => 2);
// 或
type A = ((arg: 1) => 0) & ((arg: 2) => 0));
取出函数交叉类型的最后一个值
type A = (() => 1) & (() => 2) extends () => infer I ? I : never;
// 或
type A = ((arg: 1) => 0) & ((arg: 2) => 0)) extends (arg: infer I) => 0
? I : never;
type UnionToIntersectionFn<U> = (
U extends unknown ? (k: () => U) => void : never
) extends (k: infer I) => void
? I
: never;
type GetUnionLast<U> = UnionToIntersectionFn<U> extends () => infer I
? I
: never;
type Prepend<Tuple extends unknown[], First> = [First, ...Tuple];
type UnionToTuple<
Union,
T extends unknown[] = [],
Last = GetUnionLast<Union>
> = [Union] extends [never]
? T
: UnionToTuple<Exclude<Union, Last>, Prepend<T, Last>>;
参考文章:TS 类型转换:union(联合)转 tuple(元组)
第一步:实现 UnionToIntersectionFn
类型
ts
没有提供从联合类型取值的操作
type Union = 1 | 2;
type A = Union[1]; // error
也不能直接将联合类型转为元组
type Union = 1 | 2;
type UnionToTuple<T> = T extends infer F | infer R ? [F, R] : never;
type A = UnionToTuple<Union>; // [1, 1] | [2, 2];
就需要想其他的方法,获取联合类型某个位置的值。
将联合类型转换成函数的交叉类型,通过 infer
推断类型。
在获取函数的返回值上,函数重载和函数交叉类型是一样的。
// 函数重载
type FunctionOverload = {
(): number;
(): string;
};
type A = ReturnType<FunctionOverload>; // string
// 函数交叉类型
type Intersection = (() => number) & (() => string);
type B = ReturnType<Intersection>; // string
// 函数重载和函数交叉类型相等
type C = FunctionOverload extends Intersection ? true : false; // true
实现函数交叉类型UnionToIntersectionFn
,作用:将联合类型转换成对应的函数交叉类型
你可能会想,这里两个条件判断 extends
肯定都为 true
U extends any
⇒ true
() extends (k: infer I) => void
⇒ true
那能不能省略一个或者两个条件判断呢?
不能,这里需要用到泛型的分配率
type Union = 1 | true;
type A = UnionToInfer<Union>;
// 结果=>
type A = (() => 1) & (() => true);
TIP:
type A5 = ((k: 1) => void) | ((k: 2) => void) extends (k: infer I) => void
? I
: never; // never
type A6 = ((k: () => 1) => void) | ((k: () => 2) => void) extends (
k: infer I
) => void
? I
: never; // (() => 1) & (() => 2)
k
的参数是函数,返回的类型才是交叉类型,其他的都为 never
第二步:实现 GetUnionLast
类型
GetUnionLast
作用是:获取联合类型的最后一个类型
() => infer I
可以用 {(): infer I}
代替
第三步:实现 Prepend
类型
Prepend
作用:把第二步中取到的值放在数组的第一项
接收两个参数 Tuple
和 First
,返回一个数组
Tuple
是任意类型的数组First
会任意类型的值,会放入返回数组的第一项第四步:实现 UnionToTuple
类型
接收三个参数 Union
、 T
、 Last
Union
是任意类型的联合类型T
是任意类型的数组,默认是空数组Last
是 Union
中最后一位的值,默认 GetUnionLast<Union>
通过判断联合类型中有没有 never
类型,没有 never
递归调用 UnionToTuple
。
举例: type Union = never | 1 | "s" | true | undefined;
⑴. Union = never | 1 | "s" | true | undefined
代入
[Union] extends [never]
代入 Union
[never | 1 | "s" | true | undefined] extends [never]
⇒ false
UnionToTuple<...>
语句UnionToTuple<...>
,Exclude<...>
代入Union
和 Last
,Prepend<...>
代入T
和 Last
Exclude<never | 1 | "s" | true | undefined, undefined>
⇒ true | 1 | "s"
Prepend<[], undefined>
⇒ [undefined]
UnionToTuple<true | 1 | "s", [undefined]>
,进入 ⑵⑵. Union = true | 1 | "s"
代入
[Union] extends [never]
代入 Union
[true | 1 | "s"] extends [never]
⇒ false
UnionToTuple<...>
语句UnionToTuple<...>
,Exclude<...>
代入Union
和 Last
,Prepend<...>
代入T
和 Last
Exclude<true | 1 | "s", "s">
⇒ true | 1
Prepend<[undefined], "s">
⇒ ["s", undefined]
UnionToTuple<true | 1, ["s", undefined]>
,进入 ⑶⑶. Union = true | 1
代入
[Union] extends [never]
代入 Union
[true | 1] extends [never]
⇒ false
UnionToTuple<...>
语句UnionToTuple<...>
,Exclude<...>
代入Union
和 Last
,Prepend<...>
代入T
和 Last
Exclude<true | 1, 1>
⇒ true
Prepend<["s", undefined], 1>
⇒ [1, "s", undefined]
UnionToTuple<true, [1, "s", undefined]>
,进入 ⑷⑷. Union = true
代入
[Union] extends [never]
代入 Union
[true] extends [never]
⇒ false
UnionToTuple<...>
语句UnionToTuple<...>
,Exclude<...>
代入Union
和 Last
,Prepend<...>
代入T
和 Last
Exclude<true, true>
⇒ never
Prepend<[1, "s", undefined], true>
⇒ [true, 1, "s", undefined]
UnionToTuple<never, [true, 1, "s", undefined]>
,进入 ⑸⑸. Union = never
代入
[Union] extends [never]
代入 Union
[never] extends [never]
⇒ true
T
语句T
,也就是最终的结果TIP:在低版本的 ts
中,联合类型无法调用自身,可以将条件类型包装成对象类型。
type UnionToTuple<Union, T extends any[] = [], Last = GetUnionLast<Union>> = {
0: T;
1: UnionToTuple<Exclude<Union, Last>, Prepend<T, Last>>; // false分支
}[[Union] extends [never] ? 0 : 1];
type UnionToIntersection<U> = ?((
U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never);
type IntersectionToInterFn<I> = UnionToIntersection<
I extends unknown ? () => I : never
>;
type UnionToTuple<
Union,
T extends any[] = []
> = IntersectionToInterFn<Union> extends () => infer R
? UnionToTuple<Exclude<Union, R>, [R, ...T]>
: T;
思路和方法一一样,将联合类型变为交叉函数类型: UnionToIntersection
和 IntersectionToInterFn
然后通过条件判断循环调用 UnionToTuple
得到最终的结果。
题目链接:MyAwaited
实现 MyAwaited
,用来描述 Promise
返回的类型。
import type { Equal, Expect } from "@type-challenges/utils";
type X = Promise<string>;
type Y = Promise<{ field: number }>;
type Z = Promise<Promise<string | number>>;
type cases = [
Expect<Equal<MyAwaited<X>, string>>,
Expect<Equal<MyAwaited<Y>, { field: number }>>,
Expect<Equal<MyAwaited<Z>, string | number>>
];
// @ts-expect-error
type error = MyAwaited<number>;
type MyAwaited<T extends Promise<any>> = T extends Promise<infer R>
? R extends Promise<any>
? MyAwaited<R>
: R
: never;
infer
声明 R
来获取 Promise
类型R extends Promise<any>
判断第二层是不是 Promise
类型Promise
则递归调用 MyAwaited
,Promise
则返回 Promise
类型 R
ts
提供一些内置的实用类型,可以在全局使用,官方文档:Utility Types
在做 ts
类型挑战的题目时,发现自己不会这些内置函数,所以在这里记录自己的学习过程。
infer
关键词感觉很玄乎,不太明白怎么用,特别是对类型没有概念的前端来说,类型本身就已经很绕了,再来一个类型推断,就更不能理解了。
infer
简单来讲,可以把它看成变量声明,把它想象成 let
或者 const
, infer R
可以看成 let R
或者 const R
声明一个 R
变量,后面会使用到它。
结合示例 ts
内置工具类型 Parameters
来说明
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
Parameters
作用是接收推断函数参数类型
现在有两个类型 A1
和 A2
A1
类型接收的参数是 string
类型,得到的结果就要是 string
A2
类型接收的参数是 boolean
类型,得到的结果就要是 boolean
type A1 = Parameters<(str: string) => string>; // => [string]
type A2 = Parameters<(bool: boolean) => string>; // => [boolean]
不用 infer
需要分别实现 Parameters
以满足 string
和 boolean
不同类型
// 满足 A1 类型
type Parameters<T> = T extends (...arg: string[]) => any ? string[] : never;
// 满足 A2 类型
type Parameters<T> = T extends (...arg: boolean[]) => any ? boolean[] : never;
用这种方式实现就有 2
个问题
解决这两个问题就需要用到 infer
关键字
type Parameters<T> = T extends (...arg: infer R) => any ? R : never;
用 infer
声明一个类型 R
,代表函数参数类型,你输入是啥类型就是啥类型,也就是说这里不需要写一个具体的类型了。
所以泛型 T
如果是个函数,它的参数类型就是 R
,返回的就是 R
。
infer
声明的类型只能在 true
分支中使用。
实现 Absoulte
类型,接收 string
、number
、bigint
类型,返回一个整数字符串
import type { Equal, Expect } from "@type-challenges/utils";
type cases = [
Expect<Equal<Absolute<0>, "0">>,
Expect<Equal<Absolute<-0>, "0">>,
Expect<Equal<Absolute<10>, "10">>,
Expect<Equal<Absolute<-5>, "5">>,
Expect<Equal<Absolute<"0">, "0">>,
Expect<Equal<Absolute<"-0">, "0">>,
Expect<Equal<Absolute<"10">, "10">>,
Expect<Equal<Absolute<"-5">, "5">>,
Expect<Equal<Absolute<-1_000_000n>, "1000000">>,
Expect<Equal<Absolute<9_999n>, "9999">>
];
下面三个方法的思路都是一样的
bigint
和 number
类型通过 ${T}
可以变成 string
类型
1_000_000n
= "1000000"
-{infer S}
,S
就是正数type ToString<T extends string | number | bigint> = `${T}`;
type Absolute<T extends string | number | bigint> =
ToString<T> extends `${infer P}${infer S}`
? P extends "-"
? S
: ToString<T>
: ToString<T>;
type ToString<T extends string | number | bigint> = `${T}`;
type Absolute<T extends string | number | bigint> = T extends string
? T extends `-${infer S}`
? S
: T
: Absolute<ToString<T>>;
type ToString<T extends string | number | bigint> = `${T}`;
type Absolute<T extends string | number | bigint> =
ToString<T> extends `-${infer S}` ? S : ToString<T>;
题目链接:Capitalize
实现 MyCapitalize
,满足下面要求
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<MyCapitalize<'foobar'>, 'Foobar'>>,
Expect<Equal<MyCapitalize<'FOOBAR'>, 'FOOBAR'>>,
Expect<Equal<MyCapitalize<'foo bar'>, 'Foo bar'>>,
Expect<Equal<MyCapitalize<''>, ''>>,
Expect<Equal<MyCapitalize<'a'>, 'A'>>,
Expect<Equal<MyCapitalize<'b'>, 'B'>>,
Expect<Equal<MyCapitalize<'c'>, 'C'>>,
Expect<Equal<MyCapitalize<'d'>, 'D'>>,
Expect<Equal<MyCapitalize<'e'>, 'E'>>,
Expect<Equal<MyCapitalize<'f'>, 'F'>>,
Expect<Equal<MyCapitalize<'g'>, 'G'>>,
Expect<Equal<MyCapitalize<'h'>, 'H'>>,
Expect<Equal<MyCapitalize<'i'>, 'I'>>,
Expect<Equal<MyCapitalize<'j'>, 'J'>>,
Expect<Equal<MyCapitalize<'k'>, 'K'>>,
Expect<Equal<MyCapitalize<'l'>, 'L'>>,
Expect<Equal<MyCapitalize<'m'>, 'M'>>,
Expect<Equal<MyCapitalize<'n'>, 'N'>>,
Expect<Equal<MyCapitalize<'o'>, 'O'>>,
Expect<Equal<MyCapitalize<'p'>, 'P'>>,
Expect<Equal<MyCapitalize<'q'>, 'Q'>>,
Expect<Equal<MyCapitalize<'r'>, 'R'>>,
Expect<Equal<MyCapitalize<'s'>, 'S'>>,
Expect<Equal<MyCapitalize<'t'>, 'T'>>,
Expect<Equal<MyCapitalize<'u'>, 'U'>>,
Expect<Equal<MyCapitalize<'v'>, 'V'>>,
Expect<Equal<MyCapitalize<'w'>, 'W'>>,
Expect<Equal<MyCapitalize<'x'>, 'X'>>,
Expect<Equal<MyCapitalize<'y'>, 'Y'>>,
Expect<Equal<MyCapitalize<'z'>, 'Z'>>,
]
type MyCapitalize<T extends string> = T extends `${infer F}${infer Rest}`
? `${Uppercase<F>}${Rest}`
: T;
type AlphaMap = {
a: "A";
b: "B";
c: "C";
d: "D";
e: "E";
f: "F";
g: "G";
h: "H";
i: "I";
j: "J";
k: "K";
l: "L";
n: "N";
m: "M";
o: "O";
p: "P";
q: "Q";
r: "R";
s: "S";
t: "T";
u: "U";
v: "V";
w: "W";
x: "X";
y: "Y";
z: "Z";
}
type MyUppercase<T extends string> = T extends `${infer F}${infer Rest}`
? F extends keyof AlphaMap
? `${AlphaMap[F]}${Rest}`
: T
: T;
自己实现 Uppercase
有一点需要注意:F extends keyof AlphaMap
保证 F
是小写字母
题目链接:LastOfArray,答题链接
实现 Last
,满足下面要求
import type { Equal, Expect } from "@type-challenges/utils";
type cases = [
Expect<Equal<Last<[3, 2, 1]>, 1>>,
Expect<Equal<Last<[() => 123, { a: string }]>, { a: string }>>,
Expect<Equal<Last<[]>, undefined>>
];
这题的解题思路有三种
[...rest, l]
T["length" - 1]
T["length" + 1]
type Last<T extends any[]> = T extends [...infer R, infer L] ? L : undefined;
利用剩余操作符,取出最后一项 [...rest, l]
type Last<T extends unknown[]> = [undefined, ...T][T["length"]];
利用数组长度加一: T["length" + 1]
type Last<T extends unknown[]> = T extends [infer F, ...infer R]
? T[R["length"]]
: undefined;
type LengthOfTuple<T> = T extends { length: infer N } ? N : never;
type DropFirstToTuple<T> = T extends [infer F, ...infer R] ? R : T;
type Last<T extends unknown[]> = T[LengthOfTuple<DropFirstToTuple<T>>];
方法三和方法四思路是一样的,都是利用数组长度减一: T["length" - 1]
type Last<T extends unknown[]> = T extends [infer F, ...infer R]
? R["length"] extends 0
? T[0]
: Last<R>
: undefined;
利用递归的方式,判断当前数组的长度是不是 0
,是 0
的话,说明传入的已经是空数组了,直接取 T[0]
type HasTail<T> = T extends [] | [unknown] ? false : true;
type Tail<T> = T extends [any, ...infer R] ? R : never;
type Last<T extends unknown[]> = HasTail<T> extends true ? Last<Tail<T>> : T[0];
HasTail
是利用分配率进行判断的
[] extends [] | [unknown] // true
[1] extends [] | [unknown] // true
[1, 2] extends [] | [unknown] // false
利用递归的方式,直到元组只剩最后一项,用 T[0]
的方式取出来
实现 Flatten
,将多维元组打平
import type { Equal, Expect } from "@type-challenges/utils";
type cases = [
Expect<Equal<Flatten<[]>, []>>,
Expect<Equal<Flatten<[1, 2, 3, 4]>, [1, 2, 3, 4]>>,
Expect<Equal<Flatten<[1, [2]]>, [1, 2]>>,
Expect<Equal<Flatten<[1, 2, [3, 4], [[[5]]]]>, [1, 2, 3, 4, 5]>>,
Expect<
Equal<
Flatten<[{ foo: "bar"; 2: 10 }, "foobar"]>,
[{ foo: "bar"; 2: 10 }, "foobar"]
>
>
];
type Flatten<T extends unknown[]> = T extends [infer F, ...infer Rest]
? F extends unknown[]
? [...Flatten<F>, ...Flatten<Rest>]
: [F, ...Flatten<Rest>]
: [];
// 另一种写法
type Flatten<T extends unknown[]> = T extends [infer F, ...infer Rest]
? F extends unknown[]
? Flatten<[...F, ...Rest]>
: [F, ...Flatten<Rest>]
: [];
// 另一种写法
type Flatten<T extends unknown[]> = T extends [infer F, ...infer R]
? [...(F extends unknown[] ? Flatten<F> : [F]), ...Flatten<R>]
: [];
要将多维元组变成一维元组,需要判断元组的每一项是不是元组
infer F
取出第一项,剩余的放在 ...infer Rest
中F
是不是元组,如果是元组,通过递归调用 Flatten
,将第一项打平,同时将 Rest
也打平...infer Rest
将元组剩余部分升维了...Rest
就行的,但这里为什么要使用 ...Flatten<Rest>
Rest
是元组,元组就要同步进行打平F
不是元组,就将 F
作为元组的第一项,剩余的部分接着打平 Fletten<Rest>
ps:true
分支处理 [[1], [2, 3, [4], 5]
情况,false
分支处理 [1, [2, 3, [4], 5]]
type Flatten<T> = T extends []
? []
: T extends [infer F, ...infer Rest]
? [...Flatten<F>, ...Flatten<Rest>]
: [T];
思路:如果不是元组,则将这个类型变成元组
T
是不是空元组,如果是[]
,直接返回 []
T
不是空元组,取出第一项 F
和剩余项 Rest
,分别打平false
分支最终会反应到 true
分支中ps:此方案有两个问题:
T
的类型Flatten<"tuple"> => ["tuple"]
type Flatten<T> = T extends unknown[]
? T extends [infer A, ...infer R]
? [...Flatten<A>, ...Flatten<R>]
: []
: [T];
和方法二的判断逻辑刚好相反,也存在同样的问题
T
是不是任意类型的元组题目链接:LengthOfString,解答
实现 LengthOfString
,获取字符串的长度:
import type { Equal, Expect } from "@type-challenges/utils";
type cases = [
Expect<Equal<LengthOfString<"">, 0>>,
Expect<Equal<LengthOfString<"kumiko">, 6>>,
Expect<Equal<LengthOfString<"reina">, 5>>,
Expect<Equal<LengthOfString<"Sound! Euphonium">, 16>>
];
type StringToArray<T extends string> = T extends `${infer F}${infer Rest}`
? [F, ...StringToArray<Rest>]
: [];
type LengthOfString<T extends string> = StringToArray<T>["length"];
js
中计算字符串的长度,直接用 length
就可以获取字符串的长度ts
类型中,"aa"["length"]
得到的结果是 number
length
属性的还有元组类型length
属性,不就拿到元组的长度了么type LengthOfString<
T extends string,
R extends string[] = []
> = T extends `${infer F}${infer Rest}`
? LengthOfString<Rest, [F, ...R]>
: R["length"];
type LengthOfString<T extends string, R extends string[] = []> = T extends ""
? R["length"]
: T extends `${infer F}${infer Rest}`
? LengthOfString<Rest, [F, ...R]>
: never;
方法二、方法三的思路和方法一是一样的,区别在于:
基本类型中和元组中,联合类型取的是并集,交叉类型取的是交集
type A = 1 | 2 | 3;
type B = 1 | 2 | 4;
type C = A & B; // 1 | 2
type D = A | B; // 1 | 2 | 3 | 4
在对象中,联合类型取的类型是属性交集,交叉类型取的类型是属性并集
type A = {
name: string;
age: number;
}
type B = {
name: string;
gender: string;
}
type C = A | B; // => { name: string; }
type D = A & B; // => { name: string; age: number; gender: string; }
如果换个角度,对象的联合类型是对象的并集
type A = {
name: string;
age: number;
}
type B = {
name: string;
gender: string;
}
type C = A | B; // A 或者 B
const c: C = { name: "1", age: 1, gender: "1" };
c.age = 2; // error
const c1: C = { name: "1", age: 1 };
c1.age = 2;
const c2: C = { name: "1", gender: "1" };
c2.gender = "2";
题目链接:PromiseAll
实现 PromiseAll,满足下面功能
import type { Equal, Expect } from '@type-challenges/utils'
const promiseAllTest1 = PromiseAll([1, 2, 3] as const)
const promiseAllTest2 = PromiseAll([1, 2, Promise.resolve(3)] as const)
const promiseAllTest3 = PromiseAll([1, 2, Promise.resolve(3)])
type cases = [
Expect<Equal<typeof promiseAllTest1, Promise<[1, 2, 3]>>>,
Expect<Equal<typeof promiseAllTest2, Promise<[1, 2, number]>>>,
Expect<Equal<typeof promiseAllTest3, Promise<[number, number, number]>>>,
]
type TUtil1<T> = T extends Promise<infer R> ? R : T;
type TUtil2<T extends unknown[]> = T extends [infer F, ...infer R]
? [TUtil1<F>, ...TUtil2<R>]
: [];
declare function PromiseAll<T extends unknown[]>(
values: readonly [...T]
): Promise<TUtil2<T>>;
declare function PromiseAll<T extends unknown[]>(
values: readonly [...T]
): Promise<{ [K in keyof T]: T[K] extends Promise<infer R> ? R : T[K] }>;
遍历的方法有两种:
方法一使用了可变元组类型:(Variadic tuple types)[https://github.com/microsoft/TypeScript/pull/39094]
在 ts
中,经常会用到类型约束,用来确保使用的类型是正确的,介绍 4
种类型约束
传入只能是数组,在传入的时候做约束,这种约束见的比较多
type A<T extends unknown[]>
利用 extends
条件类型进行类型约束
// 如果 T 是 string,则输出 T
// 如果 T 不是 string,则输出 never
type B<T> = T extends string ? T : never;
利用联合类型进行条件约束
// 如果 T 是 string,则输出 T
// 如果 T 不是 string,则输出 never
type C<T> = T & string;
typescript 4.7
推出了一个新的条件约束,利用 extends
语法
// F 进行了类型约束,是 string 类型
type D<T extends unknown> = T extends [
infer F extends string,
...infer Rest
] ? F : never;
// 在 4.7 之前,如果要保证 F 是 string 类型要用联合类型的形式
type D<T extends unknown> = T extends [
infer F,
...infer Rest
] ? F & string: never;
题目链接:TupleToObject
实现 TupleToObject
,将数组变为对象,对象的每项 key = value
,为数组的每项
import type { Equal, Expect } from "@type-challenges/utils";
const tuple = ["tesla", "model 3", "model X", "model Y"] as const;
type cases = [
Expect<
Equal<
TupleToObject<typeof tuple>,
{
tesla: "tesla";
"model 3": "model 3";
"model X": "model X";
"model Y": "model Y";
}
>
>
];
// @ts-expect-error
type error = TupleToObject<[[1, 2], {}]>;
type TupleToObject<T extends readonly (keyof any)[]> = {
[key in T[number]]: key;
};
type TupleToObject<T extends readonly (string | number | symbol)[]> = {
[key in T[number]]: key;
};
type TupleToObject<T extends readonly PropertyKey[]> = {
[key in T[number]]: key;
};
type TupleToObject<T extends ReadonlyArray<PropertyKey>> = {
[key in T[number]]: key;
};
keyof any
⇒ string | number | symbol
keyof
可以把 keyof
简单的理解为相遍历,把遍历的结果作为联合类型返回。
用官网的例子讲解,首先定义一个 Person
接口
interface Person {
name: string;
age: number;
location: string;
}
遍历 Person
接口的属性,将得到的值是 name
,age
,location
的联合类型
type K1 = keyof Person; // "name" | "age" | "location"
Person[]
是个数组,遍历的是数组的属性,得到的就是数组属性的联合类型
type K2 = keyof Person[]; // "length" | "push" | "pop" | "concat" | ...
就是一个对象,对象的 key
是 string
类型,所以得到 string
,但为什么是 string
和 number
的联合类型,还不太清楚。
如果把 string
换成了 number
,那么得到的结果是 nubmer
type K3 = keyof { [x: string]: Person }; // string | number
type K4 = keyof { [x: number]: Person }; // number
类型也可以通过访问的形式得到, Person["name"]
因为 name
是 string
所以得到 string
type P1 = Person["name"]; // string
name
是 string
,age
是 number
,得到 string
和 number
的联合类型
type P2 = Person["name" | "age"]; // string | number
string
的 charAt
是函数,得到 charAt
函数类型
type P3 = string["charAt"]; // (pos: number) => string
string
的 push
是函数,得到 push
函数类型
type P4 = string[]["push"]; // (...items: string[]) => number
访问数组的第一项,类型是 string
type P5 = string[][0]; // string
题目链接:UnionToIntersection
实现 UnionToIntersection
,将联合类型变为交叉类型
import type { Equal, Expect } from "@type-challenges/utils";
type cases = [
Expect<Equal<UnionToIntersection<"foo" | 42 | true>, "foo" & 42 & true>>,
Expect<
Equal<
UnionToIntersection<(() => "foo") | ((i: 42) => true)>,
(() => "foo") & ((i: 42) => true)
>
>
];
利用逆变的原理:联合类型 → 交叉类型函数 → 交叉类型
type UnionToIntersection<U> = (
U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never;
将联合类型 U
变为 (k: U) => void
:U extends unknown ? (k: U) => void : never
"foo" | 42 | true -> ((k: "foo") => void) | ((k: 42) => void) | ((k: true) => void)
根据函数的逆变可以得到
(k: "foo") => void <= (k: "foo" & 42 & true) => void
(k: 42) => void <= (k: "foo" & 42 & true) => void
(k: true) => void <= (k: "foo" & 42 & true) => void
最后类型推导的结果是 "foo" & 42 & true
type UnionToIntersection<U> = {
[key in U as symbol]: (k: U) => void;
} extends {
[k: symbol]: (k: infer I) => void;
}
? I
: never;
和方法一是一样的,它是用对象的形式去表达交叉类型函数
这个方法在文档里有介绍:Type inference in conditional types
type
和 interface
区别utility type
要用 type
定义interface
定义题目链接:MyReturnType
实现 ReturnType
返回函数的返回类型
import type { Equal, Expect } from "@type-challenges/utils";
type cases = [
Expect<Equal<string, MyReturnType<() => string>>>,
Expect<Equal<123, MyReturnType<() => 123>>>,
Expect<Equal<ComplexObject, MyReturnType<() => ComplexObject>>>,
Expect<Equal<Promise<boolean>, MyReturnType<() => Promise<boolean>>>>,
Expect<Equal<() => "foo", MyReturnType<() => () => "foo">>>,
Expect<Equal<1 | 2, MyReturnType<typeof fn>>>,
Expect<Equal<1 | 2, MyReturnType<typeof fn1>>>
];
type ComplexObject = {
a: [12, "foo"];
bar: "hello";
prev(): number;
};
const fn = (v: boolean) => (v ? 1 : 2);
const fn1 = (v: boolean, w: any) => (v ? 1 : 2);
type MyReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : never;
type MyReturnType<T> = T extends (...args: any) => infer R ? R : never;
方法一和方法二是一样的, MyReturnType
泛型 T
越不约束都行,因为后面都要进行约束,也就是说方法一更严谨一点。
协变和逆变的概念网上一搜一大把,就不说了,说概念一定会把人绕晕,举个例子来说就容易理解了。
假设现在有个父子关系: Parent
和 Son
interface Parent {
parentProp: string;
}
interface Son extends Parent {
sonProp: string;
}
type IsCo = Son extends Parent ? true : false; // true
变化规则为:f
⇒ 把 Parent
和 Son
变为数组:
type ParentArray = Parent[];
type SonArray = Son[];
type IsCo = SonArray extends ParentArray ? true : false; // true
因为 Son
是 Parent
的子类型,他们都变为数组后,还是满足 Son
是 Parent
的子类型,所以这个叫做协变。
此时把变化规则 f
改为函数 ⇒ 两个函数分别接收 Parent
和 Son
:
type ParentFn = (param: Parent) => void;
type SonFn = (param: Son) => void;
type IsContra = ParentFn extends SonFn ? true : false; // true
此时他们的继承关系反过来了,因为 Parent
是 Son
的子集,或者叫安全类型,所以这叫做逆变。
就有下面的公式
f(Son) ≤ f(Parent)
变化规则 f
被称为:协变f(Parent) ≤ f(Son)
变化规则 f
被称为:逆变逆变最常见的应用是函数重载
function fn(x: never): void;
function fn(x: string): void {}
// 等价于
function fn(x: number & string): void;
function fn(x: string): void {}
never
是 string
的子类型。
never
也可以是交叉类型的结果,如 number & string
→ never
。
题目链接:UnionToIntersection
import type { Equal, Expect } from "@type-challenges/utils";
type cases = [
Expect<Equal<UnionToIntersection<"foo" | 42 | true>, "foo" & 42 & true>>,
Expect<
Equal<
UnionToIntersection<(() => "foo") | ((i: 42) => true)>,
(() => "foo") & ((i: 42) => true)
>
>
];
答案为
type UnionToIntersection<U> = (
U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never;
首先将联合类型变为 (k: Union) => void
的形式:
"foo" | 42 | true -> ((k: "foo") => void) | ((k: 42) => void) | ((k: true) => void)
根据函数的逆变,可以得到
(k: "foo") => void <= (k: "foo" & 42 & true) => void
(k: 42) => void <= (k: "foo" & 42 & true) => void
(k: true) => void <= (k: "foo" & 42 & true) => void
最后类型推导的结果是 "foo" & 42 & true
其实看到最后的结果是有点懵的,为什么就是交叉类型呢?
把它想象成函数的重载,就好理解了
function fn(k: "foo" & 42 & true): void;
function fn(k: "foo"): void {}
// 42 和 true 同样的道理
传入什么类型可以保证 "foo"
类型安全呢?
一定是 "foo"
的子类型
那 "foo"
的子类型是什么呢?
是 never
如何得到 never
呢?
"foo" & 42 & true
要明确一个概念交叉类型 IntersectionType
是交集,联合类型 UnionType
是并集
所以 string & number & boolean
< string
< string | number | boolean
狗是动物,所以一群狗是一群动物,这是协变。我需要一张能刷人民币的信用卡,你给了我一张任意币种信用卡,这是符合要求的,即,人民币是任意货币的一种,任意币种信用卡是能刷人民币的信用卡,这个推论反过来了,叫逆变。
LeftType extends RightType ? TrueType : FalseType;
判断 LeftType
能否分配给 RightType
,RightType
一切约束条件,LeftType
都具备,也就是说 RightType
有的值 LeftType
必须要有。
type A = {
name: string;
};
type B = {
name: string;
};
type Bool = A extends B ? true : false; // Bool -> true
这里面 A
类型和 B
类型完全相同,两个类型都有 name
属性,所以输出为 true
type A = {
name: string;
};
type B = {
name: string;
age: number;
};
type Bool = A extends B ? true : false; // Bool -> false
B
类型比 A
类型多了一个 age
属性,也就是说 A
类型不满足 B
类型的所有条件,所以输出为 false
type A1 = "x" extends "x" ? true : false; // A1 -> true
type A2 = "x" | "y" extends "x" ? true : false; // A2 -> false
这个很好理解,和上面普通条件类型是一样的。
"x" | "y" extends "x"
竖线是或的意思,这里 "y" extends "x"
是 false
,所以结果是 false
。
type P<T> = T extends "x" ? true : false;
type A3 = P<"x">; // A3 -> true
type A4 = P<"x" | "y">; // A4 -> boolen
P
是带参数 T
的泛型类型,其表达式和 A1
、A2
形式完全一样。 A3
和 A4
是泛型类型 P
传入参数得到的类型, 代入参数后的形式和 A1
、A2
形式完全一样,为什么 A4
的结果是 boolean
不是 false
呢?
type P<T> = T extends "x" ? true : false;
type A4 = P<"x" | "y">;
/** 代入过程 **/
type A4 = P<"x"> | P<"y">;
// =>
type A4 = ("x" extends "x" ? true : false) | ("y" extends "x" ? true : false);
因为 T extends "x"
会应用分配率,将 "x"
和 "y"
拆开,分别代入 P<T>
,最后将结果联合起来。
// never 是所有类型的子类型
type A5 = never extends "x" ? true : false; // A5 -> true
type P<T> = T extends "x" ? true : false;
type A6 = P<never>; // A6 -> never
A6
的结果和 A5
居然不一样,因为 never
是所有类型的子类型(包括联合类型),所以它还是满足分配率的,只是 never
没有类型可以分配,也就没有执行后面的表达式,结果当然就是 never
了。
type P<T> = [T] extends ["x"] ? true : false;
type A4 = P<"x" | "y">; // A4 -> false;
type A6 = P<never>; // A6 -> true;
用 []
括起来,此时传入的泛型参数将被当做一个整体,不在分配。
要注意的是 extends
左右两个类型都要加上 []
,否则没有用。
要满足分配率有两个条件:
题目链接:AppendArgument
实现 AppendArgument
,满足下面需求,将第二个参数放到函数参数的最后一位
import type { Equal, Expect } from "@type-challenges/utils";
type Case1 = AppendArgument<(a: number, b: string) => number, boolean>;
type Result1 = (a: number, b: string, x: boolean) => number;
type Case2 = AppendArgument<() => void, undefined>;
type Result2 = (x: undefined) => void;
type cases = [Expect<Equal<Case1, Result1>>, Expect<Equal<Case2, Result2>>];
type AppendArgument<Fn, A> = Fn extends (...args: infer Args) => infer R
? (...args: [...Args, A]) => R
: never;
infer Args
获取到参数类型args_0
、 args_1
...type AppendArgument<Fn, A> = Fn extends (...args: infer Args) => infer R
? (...args: [...rest: Args, x: A]) => R
: never;
解决方法二中,形参不是传入的形参问题。
type AppendArgument<Fn extends (...args: any) => any, A> = (
...args: [...Parameters<Fn>, A]
) => ReturnType<Fn>;
Parameters
可以获取到函数的参数ReturnType
可以获取到函数的返回值题目链接:MyParameters
实现 MyParameters
,获取函数的参数类型
import type { Equal, Expect } from "@type-challenges/utils";
const foo = (arg1: string, arg2: number): void => {};
const bar = (arg1: boolean, arg2: { a: "A" }): void => {};
const baz = (): void => {};
type cases = [
Expect<Equal<MyParameters<typeof foo>, [string, number]>>,
Expect<Equal<MyParameters<typeof bar>, [boolean, { a: "A" }]>>,
Expect<Equal<MyParameters<typeof baz>, []>>
];
type MyParameters<Fn> = Fn extends (...args: infer Args) => void ? Args : never;
题目链接:GetRequired
实现 GetRequired
,返回所有的必需字段。
import type { Equal, Expect } from "@type-challenges/utils";
type cases = [
Expect<Equal<GetRequired<{ foo: number; bar?: string }>, { foo: number }>>,
Expect<
Equal<GetRequired<{ foo: undefined; bar?: undefined }>, { foo: undefined }>
>
];
用 Person
类型举例
type Person = {
name: string;
age?: number;
};
type MapToKeys<T> = {
[K in keyof T]: K;
};
type FilterKeys<T> = {
[K in keyof T]-?: undefined extends T[K] ? never : K;
}[keyof T];
type GetRequired<T> = {
[K in FilterKeys<MapToKeys<T>>]: T[K];
};
type MapToNever<T> = {
[K in keyof T]: never;
};
type PickNever<T> = {
[K in keyof T]-?: T[K] extends never ? K : never;
}[keyof T];
type GetRequired<T> = {
[K in PickNever<MapToNever<T>>]: T[K];
};
上下两种方法是一样的,这里讲左边的方法。
MapToKeys
作用把 key = value
type T1 = MapToKeys<Person>
// 结果 =>
T1 = {
name: "name";
age?: "age" | undefined;
}
FilterKeys
将可选的 key
过滤掉
type T2 = FilterKeys<Person>;
// 结果 =>
T2 = string;
解析
{[K in keyof T]-?: undefined extends T[K] ? never : K}
keyof T
的结果是联合类型,这里是 "name" | "age"
K in keyof T
中 K
结果是 name
或者 age
-?
是去除可选项{ name: string; age: never; }
undefined extends number | undefined
⇒ true
undefined extends string
⇒ false
a
的结果上加 [keyof T]
结果是 string | never
⇒ string
使用 MapToKeys
将 Person
的 key = value
,这样 b
的结果就是 name
{[K in 第二步的结果]: T[K]}
遍历取值,就得到最终的结果
-?
将可选项去掉,让这个类型变成必选项,+?
增加可选项,让这个类型变成可选项undefined extends T[K]
,下面使用 T[K] extends never
[K in keyof T]: string
K = string | undefined
K = string
[K in keyof T]: never
K = undefined
K = never
T[K]
要放到 extends
右边,下面的 T[K]
要放到 extends
的左边type GetRequired<T> = Pick<
T,
{ [key in keyof T]-?: {} extends Pick<T, key> ? never : key }[keyof T]
>;
思路和方法一是一样:
这里使用 Pick<T, key>
在 T
中挑选出 key
解析: [key in keyof T]-?: {} extends Pick<T, key> ? never : key
{} extends {age?: number}
⇒ true
{} extends {name: string}
⇒ false
Pick<T, keyof T>
是因为 keyof T
的结果是个联合类型,也就是说 Pick<T, keyof T> = T
{[key in keyof T]-?: {} extends Pick<T, key> ? never : key}[keyof T]
结果是
{name: string; age: never}["name" | "age"]
⇒ string | never
⇒ string
使用 Pick<T, 第二步的结果>
得到最终的结果
type GetRequired<T> = {
[key in keyof T as {} extends Pick<T, key> ? never : key]: T[key];
};
Pick<T, key>
在 T
中挑选出 key
{} extends Pick<T, key>
⇒ true
说明空对象和纯可选是一致的as
可以拆分成两部分,as
左边是结果,右边是约束条件
key = age
时,{} extends Pick<Person, "age">
⇒ true
⇒ {[never]: Person["age"]}
⇒ 空key = name
时,{} extends Pick<Person, "name">
⇒ false
⇒ {["name"]: Person["name"]}
⇒ {name: string}
type GetRequired<T> = {
[key in keyof T as T[key] extends Required<T>[key] ? key : never]: T[key];
};
// 等价于
type GetRequired<T, P extends Required<T> = Required<T>> = {
[key in keyof T as T[key] extends P[key] ? key : never]: T[key];
};
// 等价于
type GetRequired<T> = {
[key in keyof T as T[key] extends { [K in keyof T]-?: T[K] }[key]
? key
: never]: T[key];
};
// 等价于
type MyRequired<T> = { [K in keyof T]-?: T[K] };
type GetRequired<T, P extends MyRequired<T> = MyRequired<T>> = {
[key in keyof T as T[key] extends P[key] ? key : never]: T[key];
};
type PossiblyRequiredKeys<T> = {
[K in keyof T]: undefined extends T[K] ? never : K;
}[keyof T];
type DefinitelyRequiredKeys<T, R extends T = Required<T>> = {
[K in keyof R]: undefined extends R[K] ? K : never;
}[keyof R];
type GetRequired<T> = Pick<
T,
PossiblyRequiredKeys<T> | DefinitelyRequiredKeys<T>
>;
type GetRequired<T> = {
[key in keyof T extends infer K
? K extends keyof T
? {} extends Pick<T, K>
? never
: K
: never
: never]: T[key];
};
as
的用法在映射时经常会用到 as
关键词,像下面代码一样
type MappedKey<T> = {
[K in keyof T as K extends xxx ? never : K]: T[K];
};
一开始理解的是 as
左边要满足右边的条件,
其实不是的,这里 as
后面的语句使用来做筛选的
遍历时得到的 K
需要做一些特殊的处理时,比如给 K
加上前缀或后缀,筛选符合某种条件的 K
,用 as
连接条件语句
as
是 typescript 4.1 功能,文档链接: Key Remapping in Mapped Type
题目链接:MyReadonly
实现 Readonly
使得属性变为可读。
当尝试修改 title
和 description
会报错
interface Todo {
title: string;
description: string;
}
const todo: MyReadonly<Todo> = {
title: "Hey",
description: "foobar",
};
todo.title = "Hello"; // Error: cannot reassign a readonly property
todo.description = "barFoo"; // Error: cannot reassign a readonly property
type MyReadOnly<T extends {}> = { readonly [key in keyof T]: T[key] };
keyof
将 T
中的属性变成联合类型in
遍历联合类型readonly
去修饰每一个 key
type MyReadonly<T, K extends keyof T = keyof T> = {
readonly [key in K]: T[key];
};
这里做了两次约束
K extends keyof T
, K
是 T
属性的联合类型= keyof T
,如果没有为该类型指定类型参数,默认使用 keyof T
题目链接:FirstOfArray
实现一个 FirstOfArray
满足测试用例 cases
,作用是返回数组中的第一项
[3, 2, 1]
结果为 3
[() => 123, { a: string }]
结果为 () => 123
[]
结果为 never
[undefined]
结果为 undefined
import { Equal, Expect } from "@type-challenges/utils";
type cases = [
Expect<Equal<FirstOfArray<[3, 2, 1]>, 3>>,
Expect<Equal<FirstOfArray<[() => 123, { a: string }]>, () => 123>>,
Expect<Equal<FirstOfArray<[]>, never>>,
Expect<Equal<FirstOfArray<[undefined]>, undefined>>
];
type errors = [
// @ts-expect-error
FirstOfArray<"notArray">,
// @ts-expect-error
FirstOfArray<{ 0: "arrayLike" }>
];
type FirstOfArray<Arr extends any[]> =
Arr extends [infer Item, ...any[]] ? Item : never;
Arr
继承自任意类型的数组infer Item
是声明一个 Item
,类似于变量声明,在后面都可以使用[infer Item, ...any[]]
取出数组的第一项,放在变量 Item
extends
判断泛型 Arr
是否继承自 [infer Item, ...any[]]
[] extends [infer Item, ...any[]]
结果为 false
,所以输出为 never
true
,输出数组第一项type FirstOfArray<Arr extends any[]> = Arr extends [] ? never : Arr[0];
[] extens []
为 true
输出为 never
false
输出为 Arr[0]
type FirstOfArray<Arr extends any[]> = "0" extends keyof Arr ? Arr[0] : never;
keyof Arr
取出数组中的每一个 key
"0" extends keyof Arr
判断数组中有没第一项,因为空数组是没有第一项的。Arr[0]
否则为 never
type FirstOfArray<Arr extends any[]> = Arr["length"] extends 0 ? never : Arr[0];
Arr["length"]
可以获取泛型 Arr
的长度Arr["length"] extends 0
判断数组长度是否为 0
0
输出 never
,否者输出 Arr[0]
type FirstOrArray<Arr extends any[]> =
Arr[number] extends never ? never : Arr[0];
Arr[number]
可以得到数组的每一项值的联合类型
type A1<Arr> = Arr[number]
type A2: A1<[1, 2, 3]> // => 1 | 2 | 3
type A3: A1<[]> // => never
如果是个空数组 Arr<[]>
会返回 never
,通过判断 Arr[number] extends never
是否为 never
如果为 never
输出 never
否则输出 Arr[0]
type FirstOfArray<Arr extends any[]> = Arr[Extract<keyof Arr, "0">];
空数组的几种判断方法
T extends []
T extends never[]
"0" extends keyof T
T["length"] extends 0
T[number] extends never
题目链接:TupleToNestedObject,回答链接
实现 TupleToNestedObject
,符合下面要求
import type { Equal, Expect } from "@type-challenges/utils";
type cases = [
Expect<Equal<TupleToNestedObject<["a"], string>, { a: string }>>,
Expect<Equal<TupleToNestedObject<["a", "b"], number>, { a: { b: number } }>>,
Expect<
Equal<
TupleToNestedObject<["a", "b", "c"], boolean>,
{ a: { b: { c: boolean } } }
>
>,
Expect<Equal<TupleToNestedObject<[], boolean>, boolean>>
];
type TupleToNestedObject<T extends unknown[], R> = T extends [
infer F extends string,
...infer Rest
]
? { [K in F] : TupleToNestedObject<Rest, R> }
: R;
type TupleToNestedObject<T extends unknown[], R> = T extends [
infer F,
...infer Rest
]
? TupleToNestedObject<Rest, { [K in F & string]: R }>
: R;
type TupleToNestedObject<T extends unknown[], R> = T extends [
infer F,
...infer Rest
]
? Record<F & string, TupleToNestedObject<Rest, R>>
: R;
方法一、方法二、方法三的思路都是一样的,取出数组的第一项作为 key
,后面作为新的参数递归调用 TupleToNestedObject
,得到最终的 value
题目链接:PickByType,答题
实现 PickByType
,满足下面需求
import type { Equal, Expect } from "@type-challenges/utils";
interface Model {
name: string;
count: number;
isReadonly: boolean;
isEnable: boolean;
}
type cases = [
Expect<
Equal<
PickByType<Model, boolean>,
{ isReadonly: boolean; isEnable: boolean }
>
>,
Expect<Equal<PickByType<Model, string>, { name: string }>>,
Expect<Equal<PickByType<Model, number>, { count: number }>>
];
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
使用 as
可以对 K
进行过滤
type FilterKey<T, U, K extends keyof T = keyof T> = K extends K
? T[K] extends U
? K
: never
: never;
type PickByType<T, U> = {
[K in FilterKey<T, U>]: U;
};
在 ts 4.1
之前,没有 as
的情况下如果对 K
进行过滤?
FilterKey
类型,它的核心是利用泛型的分配:K extends K
,前面的 K
会发生分配type PickByType<T, U> = Pick<
T,
{ [K in keyof T]: T[K] extends U ? K : never }[keyof T]
>;
题目链接:MyPick
实现一个 MyPick
满足测试用例 cases
Expected1
满足一个属性:title
Expected2
满足两个属性:title
和 completed
import { Equal, Expect } from "@type-challenges/utils";
type cases = [
Expect<Equal<Expected1, MyPick<Todo, "title">>>,
Expect<Equal<Expected2, MyPick<Todo, "title" | "completed">>>,
// @ts-expect-error
MyPick<Todo, "title" | "completed" | "invalid">
];
interface Todo {
title: string;
description: string;
completed: boolean;
}
interface Expected1 {
title: string;
}
interface Expected2 {
title: string;
completed: boolean;
}
type MyPick<Type, Keys extends keyof Type> = {
[key in Keys]: Type[key];
};
使用 in
遍历 Keys
,和对象一样的取值方法 Type[key]
type MyPick<Type, Keys extends keyof Type> = Omit<Type, keyof Omit<Type, Keys>>;
使用 Omit
做两次剔除,实现 Pick
的效果了
Omit<Type, Keys>
剔除掉 Type
中 Keys
属性Omit<Type, keyof Omit<Type, Keys>>
剔除掉 1
中的结果type MyPick<Type, Keys extends keyof Type> = Omit<
Type,
Exclude<keyof Type, Keys>
>;
使用 Exclude
和 Omit
做两次剔除,实现 Pick
的效果
Exclude<keyof Type, Keys>
剔除掉 Type
中的 Keys
Omit<Type, Exclude<keyof Type, Keys>>
剔除掉 1
中的结果type MyPick<Type, Keys> = {
[key in keyof Type & Keys]: Type[key];
};
这个方法有个问题,如果 Keys
不是 Type
的键时,它不会报错。
type invalid = MyPick<Todo, "title" | "something">;
type MyPick<Type, Keys> = {
[key in keyof Type as key extends Keys ? key : never]: Type[key];
};
问题同上面一样
key extends Keys ? key : never
这是一个整体
如何获取到所有的 key
通过 keyof
可以获取到所有的 key
如何遍历所有的 key
使用 in
操作符,可以遍历所有的 key
type MyReadonly<T, K extends keyof T = keyof T> = {
readonly [key in K]: T[key];
};
上面的例子第二个参数 = keyof T
,它的作用是为 T
指定类型参数,默认值为 keyof T
。
keyof T
好理解,表示 T
中所有属性的联合类型。
interface Todo {
title: "string";
description: "string";
}
const todo: MyReadonly<Todo> = {
title: "hey",
description: "foobar",
};
todo.title = "hello"; // => error
todo.description = "barfoo"; // => error
const todo2: MyReadonly<Todo, "title"> = {
title: "hey",
description: "foobar", // => error
};
todo2.title = "hello"; // => error
todo
没有传入第二个参数,默认的约束是 title
和 description
,必须要传入 title
和 description
。
todo2
传入了第二个参数 title
,此时的约束变成了 title
,如果在 todo2
再写一个 description
将报错。
题目链接:MyOmit
实现一个 MyOmit
满足测试用例 cases
Expected1
满足两个属性:title
和 completed
Expected2
满足一个属性:title
type cases = [
Expect<Equal<Expected1, MyOmit<Todo, "description">>>,
Expect<Equal<Expected2, MyOmit<Todo, "description" | "completed">>>
];
// @ts-expect-error
type error = MyOmit<Todo, "description" | "invalid">;
interface Todo {
title: string;
description: string;
completed: boolean;
}
interface Expected1 {
title: string;
completed: boolean;
}
interface Expected2 {
title: string;
}
type MyOmit<Type, Keys extends keyof Type> = Pick<
Type,
Exclude<keyof Type, Keys>
>;
使用 Pick
和 Exclude
可以实现 Omit
Exclude<keyof Type, Keys>
使用 Exclude
剔除掉 Type
中的 key
Pick<Type, Exclude<keyof Type, Keys>>
使用 Pick
挑选出 1
中的结果type MyOmit<Type, Keys extends keyof Type> = {
[key in Exclude<keyof Type, Keys>]: Type[key];
};
使用 Exclude
可以实现 Omit
Exclude<keyof Type, Keys>
使用 Exclude
剔除掉 Type
中的 Keys
in
遍历 1
中的结果Type[key]
使用 keyof
对 any
和 never
取值
keyof any; // string | number | symbol
keyof never; // string | number | symbol
使用 keyof
对 {}
、unknown
、undefined
、null
、object
、 () => void
取值
keyof unknown; // never
keyof undefined; // never
keyof null; // never
keyof object; // never
keyof {}; // never
keyof (() => void); // never
使用 keyof
对 string
取值
keyof string;
// number | typeof Symbol.iterator | "toString" | "valueOf" | "charAt" | "charCodeAt" | "concat" | "indexOf" | "lastIndexOf" | "localeCompare" | "match" | "replace" | "search" | "slice" | "split" | "substring" | "toLowerCase" | "toLocaleLowerCase" | "toUpperCase" | "toLocaleUpperCase" | "trim" | "length" | "substr" | "codePointAt" | "includes" | "endsWith" | "normalize" | "repeat" | "startsWith" | "anchor" | "big" | "blink" | "bold" | "fixed" | "fontcolor" | "fontsize" | "italics" | "link" | "small" | "strike" | "sub" | "sup"
使用 keyof
对 number
取值
keyof number; // "toString" | "toLocaleString" | "valueOf" | "toFixed" | "toExponential" | "toPrecision"
使用 keyof
对 symbol
取值
keyof symbol; // "toString" | "valueOf" | typeof Symbol.toPrimitive | typeof Symbol.toStringTag
使用 keyof
对 boolean
取值
keyof boolean; // "valueOf"
题目链接:GetOptional
实现 GetOptional
,返回所有的可选属性
import type { Equal, Expect } from "@type-challenges/utils";
type cases = [
Expect<Equal<GetOptional<{ foo: number; bar?: string }>, { bar?: string }>>,
Expect<
Equal<GetOptional<{ foo: undefined; bar?: undefined }>, { bar?: undefined }>
>
];
这题和 [GetRequired](https://github.com/astak16/blog-ts-challenges/issues/12)
类似
type GetOptional<T> = {
[key in keyof T as {} extends Pick<T, key> ? key : never]: T[key];
};
利用 空对象 = 纯可选
,{} extends Pick<T, key>
⇒ true
⇒ key
为可选
type GetOptional<T> = Pick<
T,
{ [key in keyof T]: {} extends Pick<T, key> ? key : never }[keyof T]
>;
思路和方法一是一样的
type GetOptional<T> = {
[key in keyof T as T[key] extends Required<T>[key] ? never : key]: T[key];
};
type GetOptional<T, P extends T = Required<T>> = {
[key in keyof T as T[key] extends P[key] ? never : key]: T[key];
};
type MyRequired<T> = { [K in keyof T]-?: T[K] };
type GetOptional<T> = {
[key in keyof T as T[key] extends MyRequired<T>[key] ? never : key]: T[key];
};
利用 Required
将可选变为必选, T[key] extends Required<T>[key]
⇒ false
⇒ key
为可选。
可以自己实现一个 MyRequried
。
type MapToKeys<T> = {
[K in keyof T]: K;
};
type FilterKeys<T> = {
[K in keyof T]: undefined extends T[K] ? K : never;
}[keyof T];
type GetOptional<T> = {
[K in FilterKeys<MapToKeys<T>>]?: T[K];
};
type GetOptional<T> = Omit<T, keyof GetRequired<T>>;
利用 GetRequired
获取所有必选的 key
,使用 Omit
剔除掉。
type GetOptional<T> = {
[K in keyof T as Omit<T, K> extends T ? K : never]: T[K];
};
在一个对象中剔除掉一个可选的属性,还是属于这个对象
Omit<T, K>
其中 K
是可选属性,所以 Omit<T, K> extends T
⇒ true
type FilterOptionalKey<T, K extends keyof T> = T extends {
[k in K]-?: T[k];
}
? never
: K;
type GetOptional<T> = {
[K in keyof T as FilterOptionalKey<T, K>]: T[K];
};
T extends {[k in K]-?: T[k]}
,其中 K extends keyof T
k
是可选项, T extends {[k in K]-?: T[k]}
⇒ false
k
是必选项, T extends {[k in K]-?: T[k]}
⇒ true
{} extends Pick<T, K>
⇒ true
,K
是必选属性Omit<T, K> extends T
⇒ true
, K
是可选属性实现 TrimLeft
,满足以下要求
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<TrimLeft<'str'>, 'str'>>,
Expect<Equal<TrimLeft<' str'>, 'str'>>,
Expect<Equal<TrimLeft<' str'>, 'str'>>,
Expect<Equal<TrimLeft<' str '>, 'str '>>,
Expect<Equal<TrimLeft<' \n\t foo bar '>, 'foo bar '>>,
Expect<Equal<TrimLeft<''>, ''>>,
Expect<Equal<TrimLeft<' \n\t'>, ''>>,
]
type Space = `${" " | "\t" | "\n"}`;
type TrimLeft<T extends string> = T extends `${Space}${infer R}`
? TrimLeft<R>
: T;
type TrimLeft<T extends string> = T extends ` ${infer R}`
? TrimLeft<R>
: T extends `\t${infer R}`
? TrimLeft<R>
: T extends `\n${infer R}`
? TrimLeft<R>
: T;
type Space = `${" " | "\t" | "\n"}`;
type Trim<T extends string> = T extends
| `${Space}${infer R}`
| `${infer R}${Space}`
? Trim<R>
: T;
type Space = `${" " | "\t" | "\n"}`;
type TrimRight<T extends string> = T extends `${infer R}${Space}`
? TrimRight<R>
: T;
题目链接:If
实现 If
,接收三个参数,第一个参数是条件,只能够是 boolean
类型,后面两个参数为任意类型,如果第一个参数为 true
则返回第二个参数,如果第一个参数为 false
,则返回第三个参数。
import type { Equal, Expect } from "@type-challenges/utils";
type cases = [
Expect<Equal<If<true, "a", "b">, "a">>,
Expect<Equal<If<false, "a", 2>, 2>>
];
// @ts-expect-error
type error = If<null, "a", "b">;
type If<C extends boolean, T, F> = C extends true ? T : F;
这题有个 ts-expect-error
,所以第一个参数需要约束,只能传入 boolean
类型。
题目链接:ChainableOption,回答链接
实现 Chainable
,满足下面要求
import type { Alike, Expect } from "@type-challenges/utils";
declare const a: Chainable;
const result1 = a
.option("foo", 123)
.option("bar", { value: "Hello World" })
.option("name", "type-challenges")
.get();
const result2 = a
.option("name", "another name")
// @ts-expect-error
.option("name", "last name")
.get();
type cases = [
Expect<Alike<typeof result1, Expected1>>,
Expect<Alike<typeof result2, Expected2>>
];
type Expected1 = {
foo: number;
bar: {
value: string;
};
name: string;
};
type Expected2 = {
name: string;
};
type Chainable<T = unknown> = {
option<K extends string, V>(
key: K extends keyof T ? never : K,
value: V
): Chainable<T & { [k in K]: V }>;
get(): T;
};
用一个例子来讲解
declare const a: Chainable;
const result1 = a
.option("foo", 123)
.option("bar", { value: "Hello World" })
.get();
type Result1 = {
foo: number;
};
第一个 option
函数内部推导
1. T = unknown, keyof T = never, K = "foo", V = 123
2. key = "foo" extends never => false => "foo", value = 123
3. T & {[k in K]: V} = unknown & {foo: 123} = {foo: 123}
Chainable<{foo: 123}>
T = {foo: 123}
第二个 option
函数内部推导
// T 是第一个 option 函数的结果
1. T = {foo: 123}, keyof T = "foo", K = "bar", V = { value: "Hello World" }
2. key = "bar" extends "foo" => false => "bar", value = { value: "Hello World" }
3. T & {[k in K]: V} = {foo: 123} & {bar: { value: "Hello World" }}
Chainable<{foo: 123} & {bar: { value: "Hello World" }}>
T = {foo: 123} & {bar: { value: "Hello World" }}
调用 get
得到结果,返回 T
如果调用 option
传入相同的 key
时,应该要报错,看下面的例子
const result2 = a
.option("name", "another name")
.option("name", "last name")
.get();
type Result2 = {
name: string;
};
第一个 option
推导出的结果是 {name: "another name"}
第二个 option
的 key
得到的值为 never
K extends keyof T ? never : K => "name" extends "name" => true => K = never
Chainable<{name: "another name"}>
T = {name: "another name"}
调用 get
得到结果,返回 T
type Chainable<T = unknown> = {
option<K extends string, V>(
key: K extends keyof T ? never : K,
value: V
): Chainable<{ [P in K | keyof T]: P extends keyof T ? T[P] : V }>;
get(): T;
};
用例子来讲解一步步的推算过程
declare const a: Chainable;
const result1 = a
.option("foo", 123)
.option("bar", { value: "Hello World" })
.get();
type Result1 = {
foo: number;
};
第一个 option
函数内部推导
1. T = unknown, keyof T = never, K = "foo", V = 123
2. key = "foo" extends never => false => "foo", value = 123
3. 对象左边 = K | keyof T => "foo" | never => P = "foo"
对象右边 = "foo" extends never => false => 123
Chainable<{foo: 123}>
T = {foo: 123}
第二个 option
函数内部推导
// T 是第一个 option 函数的结果
1. T = {foo: 123}, keyof T = "foo", K = "bar", V = { value: "Hello World" }
2. key = "bar" extends "foo" => false => "bar", value = { value: "Hello World" }
3. 对象左边 = K | keyof T => "bar" | "foo" => P = "bar" 或者 "foo"
对象右边 =
当 P = "bar" 时,"bar" extends "foo" =? false => { value: "Hello World" }
当 P = "foo" 时,"foo" extends "foo" =? true => 123
Chainable<{bar: { value: "Hello World" }; foo: 123}>
T = {bar: { value: "Hello World" }; foo: 123}
调用 get
得到结果,返回 T
如果调用 option
传入相同的 key
时,应该要报错,看下面的例子
const result2 = a
.option("name", "another name")
.option("name", "last name")
.get();
type Result2 = {
name: string;
};
第一个 option
推导出的结果是 {name: "another name"}
第二个 option
的 key
得到的值为 never
K extends keyof T ? never : K => "name" extends "name" => true => K = never
Chainable<{name: "another name"}>
T = {name: "another name"}
调用 get
得到结果,返回 T
type Chainable = {
option<T, K extends string, V>(
this: T,
key: K extends keyof T ? never : K,
value: V
): T & { [key in K]: V };
get<T>(this: T): Omit<T, "option" | "get">;
};
这里的 this
指代 Chainable
本身,由于它本身包含了 option
和 get
两个方法,所以要用 Omit
把这两个方法去除掉。
{ [k in K]: V }
可以用 Record<K, V>
代替K extends keyof T ? never : K
可以用 Exclude<K, keyof T>
代替题目链接:MyReadonly2
实现 MyReadonly2
,接收两个参数,把第二个参数变为只读,同时第二个参数可以不传,不传的话就把第一个参数中所有的属性都变位只读。
import type { Alike, Expect } from "@type-challenges/utils";
type cases = [
Expect<Alike<MyReadonly2<Todo1>, Readonly<Todo1>>>,
Expect<Alike<MyReadonly2<Todo1, "title" | "description">, Expected>>,
Expect<Alike<MyReadonly2<Todo2, "title" | "description">, Expected>>
];
interface Todo1 {
title: string;
description?: string;
completed: boolean;
}
interface Todo2 {
readonly title: string;
description?: string;
completed: boolean;
}
interface Expected {
readonly title: string;
readonly description?: string;
completed: boolean;
}
实现 MyReadonly2
类型,把 T
变位只读,用 Omit
从 T
中挑选出 K
,最后使用 &
连接
type MyReadonly2<T, K extends keyof T = keyof T> = {
readonly [key in K]: T[key];
} & Omit<T, K>;
type MyReadonly2<T, K extends keyof T = keyof T> = {
readonly [key in K]: T[key];
} & {
[key in K as key extends K ? key : never]: T[key];
};
type MyReadonly2<T, K extends keyof T = keyof T> = Readonly<T> & Omit<T, K>;
题目链接:Replace
实现 Replace
,满足下面需求
import type { Equal, Expect } from "@type-challenges/utils";
type cases = [
Expect<Equal<Replace<"foobar", "bar", "foo">, "foofoo">>,
Expect<Equal<Replace<"foobarbar", "bar", "foo">, "foofoobar">>,
Expect<Equal<Replace<"foobarbar", "", "foo">, "foobarbar">>,
Expect<Equal<Replace<"foobarbar", "bar", "">, "foobar">>,
Expect<Equal<Replace<"foobarbar", "bra", "foo">, "foobarbar">>,
Expect<Equal<Replace<"", "", "">, "">>
];
type Replace<
R extends string,
From extends string,
To extends string
> = From extends ""
? R
: R extends `${infer A}${From}${infer B}`
? `${A}${To}${B}`
: R;
要想在 R
中拿到 From
,可以把 R
拆分为 ${infer A}${From}${infer B}
,然后把 From
替换为 To
就可以了。
题目链接:ReplaceAll
实现 ReplaceAll
,满足下面需求
import type { Equal, Expect } from "@type-challenges/utils";
type cases = [
Expect<Equal<ReplaceAll<"foobar", "bar", "foo">, "foofoo">>,
Expect<Equal<ReplaceAll<"foobar", "bag", "foo">, "foobar">>,
Expect<Equal<ReplaceAll<"foobarbar", "bar", "foo">, "foofoofoo">>,
Expect<Equal<ReplaceAll<"t y p e s", " ", "">, "types">>,
Expect<Equal<ReplaceAll<"foobarbar", "", "foo">, "foobarbar">>,
Expect<Equal<ReplaceAll<"barfoo", "bar", "foo">, "foofoo">>,
Expect<Equal<ReplaceAll<"foobarfoobar", "ob", "b">, "fobarfobar">>,
Expect<Equal<ReplaceAll<"foboorfoboar", "bo", "b">, "foborfobar">>,
Expect<Equal<ReplaceAll<"", "", "">, "">>
];
type ReplaceAll<
R extends string,
From extends string,
To extends string
> = From extends ""
? R
: R extends `${infer A}${From}${infer B}`
? `${A}${To}${ReplaceAll<B, From, To>}`
: R;
和 Replace
思路是一样的,只是 Replace
只把第一个 From
替换成 To
,ReplaceAll
需要把所有的 From
都替换成 To
,所有就需要递归去找 From
。
题目链接:Concat
实现 Concat
,它接收两个参数,两个参数的类型都是数组,将两个数组进行连接,并返回它。
import type { Equal, Expect } from "@type-challenges/utils";
type cases = [
Expect<Equal<Concat<[], []>, []>>,
Expect<Equal<Concat<[], [1]>, [1]>>,
Expect<Equal<Concat<[1, 2], [3, 4]>, [1, 2, 3, 4]>>,
Expect<
Equal<
Concat<["1", 2, "3"], [false, boolean, "4"]>,
["1", 2, "3", false, boolean, "4"]
>
>
];
type Concat<T extends readonly unknown[], F extends readonly unknown[]> = [
...T,
...U
];
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.