2020 年阿宝哥写了 《重学 TS 1.0》 系列文章,共写了 50 几篇。TS 发展很快,越来越多的项目都开始使用 TS 来开发,目前 TS 的版本已经更新到了 4.4 版本。
近期阿宝哥在团队内培训 TS 专题,发现一些小伙伴对 TS 掌握得并不是很好,对 TS 类型编程(类型体操)的相关概念理解并不是很清楚。所以阿宝哥决定开始更新 《重学 TS 2.0》,这个专题会对之前的文章进行补充与更新,同时会介绍 TS 4.0 以上的新特性。同时,也会重点介绍 TS 中的一些核心概念,比如类型安全、结构类型系统、映射类型、泛型、逆变/协变等。
在 《重学 TS 2.0》 系列开始之前,大家先来做几道题热热身,感兴趣的小伙伴可以动手试试看。这 30 道题的部分参考答案,已经提交到这个仓库👇。
答题时可以考虑直接使用在线 TS Playground,选用最新的编译器版本:v4.4.2。
第一题
type User = {
id: number;
kind: string;
};
function makeCustomer<T extends User>(u: T): T {
// Error(TS 编译器版本:v4.4.2)
// Type '{ id: number; kind: string; }' is not assignable to type 'T'.
// '{ id: number; kind: string; }' is assignable to the constraint of type 'T',
// but 'T' could be instantiated with a different subtype of constraint 'User'.
return {
id: u.id,
kind: 'customer'
}
}
以上代码为什么会提示错误,应该如何解决上述问题?
第二题
本道题我们希望参数 a
和 b
的类型都是一致的,即 a
和 b
同时为 number
或 string
类型。当它们的类型不一致的值,TS 类型检查器能自动提示对应的错误信息。
function f(a: string | number, b: string | number) {
if (typeof a === 'string') {
return a + ':' + b; // no error but b can be number!
} else {
return a + b; // error as b can be number | string
}
}
f(2, 3); // Ok
f(1, 'a'); // Error
f('a', 2); // Error
f('a', 'b') // Ok
第三题
在 掌握 TS 这些工具类型,让你开发事半功倍 这篇文章中,阿宝哥介绍了 TS 内置的工具类型:Partial<T>
,它的作用是将某个类型里的属性全部变为可选项 ?
。
interface Todo {
title: string;
description: string;
}
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate };
}
// lib.es5.d.ts
type Partial<T> = {
[P in keyof T]?: T[P];
};
那么如何定义一个 SetOptional
工具类型,支持把给定的 keys 对应的属性变成可选的?对应的使用示例如下所示:
type Foo = {
a: number;
b?: string;
c: boolean;
}
// 测试用例
type SomeOptional = SetOptional<Foo, 'a' | 'b'>;
// type SomeOptional = {
// a?: number; // 该属性已变成可选的
// b?: string; // 保持不变
// c: boolean;
// }
在实现 SetOptional
工具类型之后,如果你感兴趣,可以继续实现 SetRequired
工具类型,利用它可以把指定的 keys 对应的属性变成必填的。对应的使用示例如下所示:
type Foo = {
a?: number;
b: string;
c?: boolean;
}
// 测试用例
type SomeRequired = SetRequired<Foo, 'b' | 'c'>;
// type SomeRequired = {
// a?: number;
// b: string; // 保持不变
// c: boolean; // 该属性已变成必填
// }
第四题
Pick<T, K extends keyof T>
的作用是将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型。
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
const todo: TodoPreview = {
title: "Clean room",
completed: false
};
那么如何定义一个 ConditionalPick
工具类型,支持根据指定的 Condition
条件来生成新的类型,对应的使用示例如下:
interface Example {
a: string;
b: string | number;
c: () => void;
d: {};
}
// 测试用例:
type StringKeysOnly = ConditionalPick<Example, string>;
//=> {a: string}
第五题
定义一个工具类型 AppendArgument
,为已有的函数类型增加指定类型的参数,新增的参数名是 x
,将作为新函数类型的第一个参数。具体的使用示例如下所示:
type Fn = (a: number, b: string) => number
type AppendArgument<F, A> = // 你的实现代码
type FinalFn = AppendArgument<Fn, boolean>
// (x: boolean, a: number, b: string) => number
第六题
定义一个 NativeFlat 工具类型,支持把数组类型拍平(扁平化)。具体的使用示例如下所示:
type NaiveFlat<T extends any[]> = // 你的实现代码
// 测试用例:
type NaiveResult = NaiveFlat<[['a'], ['b', 'c'], ['d']]>
// NaiveResult的结果: "a" | "b" | "c" | "d"
在完成 NaiveFlat
工具类型之后,在继续实现 DeepFlat
工具类型,以支持多维数组类型:
type DeepFlat<T extends any[]> = unknown // 你的实现代码
// 测试用例
type Deep = [['a'], ['b', 'c'], [['d']], [[[['e']]]]];
type DeepTestResult = DeepFlat<Deep>
// DeepTestResult: "a" | "b" | "c" | "d" | "e"
第七题
使用类型别名定义一个 EmptyObject
类型,使得该类型只允许空对象赋值:
type EmptyObject = {}
// 测试用例
const shouldPass: EmptyObject = {}; // 可以正常赋值
const shouldFail: EmptyObject = { // 将出现编译错误
prop: "TS"
}
在通过 EmptyObject
类型的测试用例检测后,我们来更改以下 takeSomeTypeOnly
函数的类型定义,让它的参数只允许严格SomeType类型的值。具体的使用示例如下所示:
type SomeType = {
prop: string
}
// 更改以下函数的类型定义,让它的参数只允许严格SomeType类型的值
function takeSomeTypeOnly(x: SomeType) { return x }
// 测试用例:
const x = { prop: 'a' };
takeSomeTypeOnly(x) // 可以正常调用
const y = { prop: 'a', addditionalProp: 'x' };
takeSomeTypeOnly(y) // 将出现编译错误
第八题
定义 NonEmptyArray
工具类型,用于确保数据非空数组。
type NonEmptyArray<T> = // 你的实现代码
const a: NonEmptyArray<string> = [] // 将出现编译错误
const b: NonEmptyArray<string> = ['Hello TS'] // 非空数据,正常使用
提示:该题目有多种解法,感兴趣小伙伴可以自行尝试一下。
第九题
定义一个 JoinStrArray
工具类型,用于根据指定的 Separator
分隔符,对字符串数组类型进行拼接。具体的使用示例如下所示:
type JoinStrArray<Arr extends string[], Separator extends string, Result extends string = ""> = // 你的实现代码
// 测试用例
type Names = ["Sem", "Lolo", "Kaquko"]
type NamesComma = JoinStrArray<Names, ","> // "Sem,Lolo,Kaquko"
type NamesSpace = JoinStrArray<Names, " "> // "Sem Lolo Kaquko"
type NamesStars = JoinStrArray<Names, "⭐️"> // "Sem⭐️Lolo⭐️Kaquko"
第十题
实现一个 Trim
工具类型,用于对字符串字面量类型进行去空格处理。具体的使用示例如下所示:
type Trim<V extends string> = // 你的实现代码
// 测试用例
Trim<' semlinker '>
//=> 'semlinker'
提示:可以考虑先定义 TrimLeft 和 TrimRight 工具类型,再组合成 Trim 工具类型。
第十一题
实现一个 IsEqual
工具类型,用于比较两个类型是否相等。具体的使用示例如下所示:
type IsEqual<A, B> = // 你的实现代码
// 测试用例
type E0 = IsEqual<1, 2>; // false
type E1 = IsEqual<{ a: 1 }, { a: 1 }> // true
type E2 = IsEqual<[1], []>; // false
第十二题
实现一个 Head
工具类型,用于获取数组类型的第一个类型。具体的使用示例如下所示:
type Head<T extends Array<any>> = // 你的实现代码
// 测试用例
type H0 = Head<[]> // never
type H1 = Head<[1]> // 1
type H2 = Head<[3, 2]> // 3
提示:该题目有多种解法,感兴趣小伙伴可以自行尝试一下。
第十三题
实现一个 Tail
工具类型,用于获取数组类型除了第一个类型外,剩余的类型。具体的使用示例如下所示:
type Tail<T extends Array<any>> = // 你的实现代码
// 测试用例
type T0 = Tail<[]> // []
type T1 = Tail<[1, 2]> // [2]
type T2 = Tail<[1, 2, 3, 4, 5]> // [2, 3, 4, 5]
提示:该题目有多种解法,感兴趣小伙伴可以自行尝试一下。
第十四题
实现一个 Unshift
工具类型,用于把指定类型 E
作为第一个元素添加到 T
数组类型中。具体的使用示例如下所示:
type Unshift<T extends any[], E> = // 你的实现代码
// 测试用例
type Arr0 = Unshift<[], 1>; // [1]
type Arr1 = Unshift<[1, 2, 3], 0>; // [0, 1, 2, 3]
提示:该题目有多种解法,感兴趣小伙伴可以自行尝试一下。
第十五题
实现一个 Shift
工具类型,用于移除 T
数组类型中的第一个类型。具体的使用示例如下所示:
type Shift<T extends any[]> = // 你的实现代码
// 测试用例
type S0 = Shift<[1, 2, 3]> // [2, 3]
type S1 = Shift<[string,number,boolean]> // [number,boolean]
第十六题
实现一个 Push
工具类型,用于把指定类型 E
作为最后一个元素添加到 T
数组类型中。具体的使用示例如下所示:
type Push<T extends any[], V> = // 你的实现代码
// 测试用例
type Arr0 = Push<[], 1> // [1]
type Arr1 = Push<[1, 2, 3], 4> // [1, 2, 3, 4]
第十七题
实现一个 Includes
工具类型,用于判断指定的类型 E
是否包含在 T
数组类型中。具体的使用示例如下所示:
type Includes<T extends Array<any>, E> = // 你的实现代码
type I0 = Includes<[], 1> // false
type I1 = Includes<[2, 2, 3, 1], 2> // true
type I2 = Includes<[2, 3, 3, 1], 1> // true
提示:该题目有多种解法,感兴趣小伙伴可以自行尝试一下。
第十八题
实现一个 UnionToIntersection
工具类型,用于把联合类型转换为交叉类型。具体的使用示例如下所示:
type UnionToIntersection<U> = // 你的实现代码
// 测试用例
type U0 = UnionToIntersection<string | number> // never
type U1 = UnionToIntersection<{ name: string } | { age: number }> // { name: string; } & { age: number; }
第十九题
实现一个 OptionalKeys
工具类型,用来获取对象类型中声明的可选属性。具体的使用示例如下所示:
type Person = {
id: string;
name: string;
age: number;
from?: string;
speak?: string;
};
type OptionalKeys<T> = // 你的实现代码
type PersonOptionalKeys = OptionalKeys<Person> // "from" | "speak"
提示:该题目有多种解法,感兴趣小伙伴可以自行尝试一下。
第二十题
实现一个 Curry
工具类型,用来实现函数类型的柯里化处理。具体的使用示例如下所示:
type Curry<
F extends (...args: any[]) => any,
P extends any[] = Parameters<F>,
R = ReturnType<F>
> = // 你的实现代码
type F0 = Curry<() => Date>; // () => Date
type F1 = Curry<(a: number) => Date>; // (arg: number) => Date
type F2 = Curry<(a: number, b: string) => Date>; // (arg_0: number) => (b: string) => Date
第二十一题
实现一个 Merge
工具类型,用于把两个类型合并成一个新的类型。第二种类型(SecondType)的 Keys
将会覆盖第一种类型(FirstType)的 Keys
。具体的使用示例如下所示:
type Foo = {
a: number;
b: string;
};
type Bar = {
b: number;
};
type Merge<FirstType, SecondType> = // 你的实现代码
const ab: Merge<Foo, Bar> = { a: 1, b: 2 };
第二十二题
实现一个 RequireAtLeastOne
工具类型,它将创建至少含有一个给定 Keys
的类型,其余的 Keys
保持原样。具体的使用示例如下所示:
type Responder = {
text?: () => string;
json?: () => string;
secure?: boolean;
};
type RequireAtLeastOne<
ObjectType,
KeysType extends keyof ObjectType = keyof ObjectType,
> = // 你的实现代码
// 表示当前类型至少包含 'text' 或 'json' 键
const responder: RequireAtLeastOne<Responder, 'text' | 'json'> = {
json: () => '{"message": "ok"}',
secure: true
};
第二十三题
实现一个 RemoveIndexSignature
工具类型,用于移除已有类型中的索引签名。具体的使用示例如下所示:
interface Foo {
[key: string]: any;
[key: number]: any;
bar(): void;
}
type RemoveIndexSignature<T> = // 你的实现代码
type FooWithOnlyBar = RemoveIndexSignature<Foo>; //{ bar: () => void; }
第二十四题
实现一个 Mutable
工具类型,用于移除对象类型上所有属性或部分属性的 readonly
修饰符。具体的使用示例如下所示:
type Foo = {
readonly a: number;
readonly b: string;
readonly c: boolean;
};
type Mutable<T, Keys extends keyof T = keyof T> = // 你的实现代码
const mutableFoo: Mutable<Foo, 'a'> = { a: 1, b: '2', c: true };
mutableFoo.a = 3; // OK
mutableFoo.b = '6'; // Cannot assign to 'b' because it is a read-only property.
第二十五题
实现一个 IsUnion
工具类型,判断指定的类型是否为联合类型。具体的使用示例如下所示:
type IsUnion<T, U = T> = // 你的实现代码
type I0 = IsUnion<string|number> // true
type I1 = IsUnion<string|never> // false
type I2 =IsUnion<string|unknown> // false
第二十六题
实现一个 IsNever
工具类型,判断指定的类型是否为 never
类型。具体的使用示例如下所示:
type I0 = IsNever<never> // true
type I1 = IsNever<never | string> // false
type I2 = IsNever<null> // false
第二十七题
实现一个 Reverse
工具类型,用于对元组类型中元素的位置颠倒,并返回该数组。元组的第一个元素会变成最后一个,最后一个元素变成第一个。
type Reverse<
T extends Array<any>,
R extends Array<any> = []
> = // 你的实现代码
type R0 = Reverse<[]> // []
type R1 = Reverse<[1, 2, 3]> // [3, 2, 1]
第二十八题
实现一个 Split
工具类型,根据给定的分隔符(Delimiter)对包含分隔符的字符串进行切割。可用于定义 String.prototype.split
方法的返回值类型。具体的使用示例如下所示:
type Item = 'semlinker,lolo,kakuqo';
type Split<
S extends string,
Delimiter extends string,
> = // 你的实现代码
type ElementType = Split<Item, ','>; // ["semlinker", "lolo", "kakuqo"]
第二十九题
实现一个 ToPath
工具类型,用于把属性访问(.
或 []
)路径转换为元组的形式。具体的使用示例如下所示:
type ToPath<S extends string> = // 你的实现代码
ToPath<'foo.bar.baz'> //=> ['foo', 'bar', 'baz']
ToPath<'foo[0].bar.baz'> //=> ['foo', '0', 'bar', 'baz']
第三十题
完善 Chainable
类型的定义,使得 TS 能成功推断出 result
变量的类型。调用 option
方法之后会不断扩展当前对象的类型,使得调用 get
方法后能获取正确的类型。
declare const config: Chainable
type Chainable = {
option(key: string, value: any): any
get(): any
}
const result = config
.option('age', 7)
.option('name', 'lolo')
.option('address', { value: 'XiaMen' })
.get()
type ResultType = typeof result
// 期望 ResultType 的类型是:
// {
// age: number
// name: string
// address: {
// value: string
// }
// }
如果以上题目你都能轻松作答,你的 TS 水平应该还不错。当然如果你做完上述题目之后,还觉得意犹未尽的话,可以继续做在线的 TS 练习题,👉 typescript-exercises.github.io/。大家如果有好的 TS 练习题,可以推荐给阿宝哥哈。
关注「全栈修仙之路」阅读阿宝哥原创的 4 本免费电子书(累计下载 5万+)及 11 篇 Vue 3 进阶系列教程。
《重学 TS 2.0》 重新出发了,感兴趣的小伙伴可以加阿宝哥的微信:semlinker,发送 TS 邀请你进群一起学习。想知道详细答案的小伙伴,也可以私聊阿宝哥。欢迎大家去提交每道题的参考答案哟。