前言
相信这段时间来,对 TypeScript 感兴趣的小伙伴们已经把这个神器给系统的学习了一遍了吧。如果计划开始学习但是还没有开始,或者没有找到资料的同学,可以看下我在之前文章中前端进阶指南[1] 找一下 TypeScript 部分的教程,自行学习。
本文从最近在 Github 上比较火的仓库 typescript-exercises[2] 入手,它的中文介绍是 「富有挑战性的 TypeScript 练习集」。里面包含了 15 个 TypeScript 的练习题,我会从其中挑选出几个比较有价值的题目,一起来解答一下。
目标
来看下这个仓库的发起者所定下的目标,让每个人都学会以下知识点的实战运用:
- Basic typing.
- Refining types.
- Union types.
- Merged types.
- Generics.
- Type declarations.
- Module augmentation.
- Advanced type mapping.
真的都是一些非常有难度且实用的知识点,掌握了它们一定会让我们在编写 TypeScript 类型的时候如虎添翼。
挑战
exercise-00
题目
import chalk from "chalk" // 这里需要补全 const users: unknown[] = [ { name: "Max Mustermann", age: 25, occupation: "Chimney sweep", }, { name: "Kate Müller", age: 23, occupation: "Astronaut", }, ] // 这里需要补全 function logPerson(user: unknown) { console.log(` - ${chalk.green(user.name)}, ${user.age}`) } console.log(chalk.yellow("Users:")) users.forEach(logPerson)
解答
第一题只是个热身题,考察对接口类型定义的掌握,直接定义 User 接口即可实现。
interface User { name: string age: number occupation: string } const users: User[] = [ { name: "Max Mustermann", age: 25, occupation: "Chimney sweep", }, { name: "Kate Müller", age: 23, occupation: "Astronaut", }, ] function logPerson(user: User) { console.log(` - ${chalk.green(user.name)}, ${user.age}`) } console.log(chalk.yellow("Users:")) users.forEach(logPerson)
或者利用类型推导,users
数组会自动推断出类型:
const users = [ { name: "Max Mustermann", age: 25, occupation: "Chimney sweep", }, { name: "Kate Müller", age: 23, occupation: "Astronaut", }, ]
在 VSCode 中,鼠标放到 users 变量上即可看到类型被自动推断出来了:
{ name: string age: number occupation: string } ;[]
那么利用 typeof
关键字,配合索引查询,我们也可以轻松取得这个类型。这里 number 的意思就是查找出 users
的所有数字下标对应的值的类型集合。
type User = typeof users[number]
这个仓库提供了每道题的答题机制,执行 npm run 0
对应题号,看到结果即可证明编译通过,答案正确。
execsise-01
题目
最初,我们在数据库中只有 User
类型,后来引入了 Admin
类型。把这两个类型组合成 Person
类型以修复错误。
interface User { name: string age: number occupation: string } interface Admin { name: string age: number role: string } const persons: User[] /* <- Person[] */ = [ { name: "Max Mustermann", age: 25, occupation: "Chimney sweep", }, { name: "Jane Doe", age: 32, role: "Administrator", }, { name: "Kate Müller", age: 23, occupation: "Astronaut", }, { name: "Bruce Willis", age: 64, role: "World saver", }, ] function logPerson(user: User) { console.log(` - ${chalk.green(user.name)}, ${user.age}`) } persons.forEach(logPerson)
解答
本题考查联合类型的使用:
// 定义联合类型 type Person = User | Admin const persons: Person[] /* <- Person[] */ = [ { name: "Max Mustermann", age: 25, occupation: "Chimney sweep", }, { name: "Jane Doe", age: 32, role: "Administrator", }, { name: "Kate Müller", age: 23, occupation: "Astronaut", }, { name: "Bruce Willis", age: 64, role: "World saver", }, ] function logPerson(user: Person) { console.log(` - ${chalk.green(user.name)}, ${user.age}`) }
exercise-02
根据上题中定义出的 Person
类型,现在需要一个方法打印出它的实例:
题目
function logPerson(person: Person) { let additionalInformation: string if (person.role) { // ❌ 报错 Person 类型中不一定有 role 属性 additionalInformation = person.role } else { // ❌ 报错 Person 类型中不一定有 occupation 属性 additionalInformation = person.occupation } console.log( ` - ${chalk.green(person.name)}, ${person.age}, ${additionalInformation}`, ) }
解答
本题考查 TypeScript 中的「类型保护」,TypeScript 的程序流分析使得某些判断代码包裹之下的代码中,类型可以被进一步收缩。
in
操作符:
function logPerson(person: Person) { let additionalInformation: string if ("role" in person) { additionalInformation = person.role } else { additionalInformation = person.occupation } console.log( ` - ${chalk.green(person.name)}, ${person.age}, ${additionalInformation}`, ) }
函数返回值中的 is
推断:
function isAdmin(user: Person): user is Admin { return user.hasOwnProperty("role") } function logPerson(person: Person) { let additionalInformation: string if (isAdmin(person)) { additionalInformation = person.role } else { additionalInformation = person.occupation } console.log( ` - ${chalk.green(person.name)}, ${person.age}, ${additionalInformation}`, ) }
exercise-04
题目
本题定义了一个 filterUsers
方法,用来通过 person
中的某些字段来筛选出用户的子集。
function filterUsers(persons: Person[], criteria: User): User[] { return persons.filter(isUser).filter((user) => { let criteriaKeys = Object.keys(criteria) as (keyof User)[] return criteriaKeys.every((fieldName) => { return user[fieldName] === criteria[fieldName] }) }) } console.log(chalk.yellow("Users of age 23:")) filterUsers( persons, // ❌ 报错,criteria 定义的是精确的 User 类型,少字段了。 { age: 23, }, ).forEach(logPerson)
可以看出,由于 filterUsers
的第二个筛选参数的类型被精确的定义为 User
,所以只传入它的一部分字段 age
就会报错。
解答
本题考查 [mapped-types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types "mapped-types")
映射类型,
type Criteria = { [K in keyof User]?: User[K] } function filterUsers(persons: Person[], criteria: Criteria): User[] { return persons.filter(isUser).filter((user) => { let criteriaKeys = Object.keys(criteria) as (keyof User)[] return criteriaKeys.every((fieldName) => { return user[fieldName] === criteria[fieldName] }) }) }
Criteria
利用了映射类型,把 User
的 key 值遍历了一遍,并且加上了 ?
标志代表字段都是可选的,即可完成任务。
也可以利用内置类型 Partial
,这个类型用于把另一个类型的字段全部转为可选。
function filterUsers(persons: Person[], criteria: Partial<User>): User[] {}
exercise-05
题目
function filterPersons( persons: Person[], personType: string, criteria: unknown, ): unknown[] {} let usersOfAge23: User[] = filterPersons(persons, "user", { age: 23 }) let adminsOfAge23: Admin[] = filterPersons(persons, "admin", { age: 23 })
解答
本题返回值类型即可以是 User
,也可以是Admin
,并且很明显这个结果是由第二个参数是'user'
还是 'admin'
所决定。
利用函数重载的功能,可以轻松实现,针对每种不同的 personType
参数类型,我们给出不同的返回值类型:
function filterPersons( persons: Person[], personType: "admin", criteria: Partial<Person>, ): Admin[] function filterPersons( persons: Person[], personType: "user", criteria: Partial<Person>, ): User[] function filterPersons( persons: Person[], personType: string, criteria: Partial<Person>, ) {} let usersOfAge23: User[] = filterPersons(persons, "user", { age: 23 }) let adminsOfAge23: Admin[] = filterPersons(persons, "admin", { age: 23 })
exercise-06
题目
function swap(v1, v2) { return [v2, v1] } function test1() { console.log(chalk.yellow("test1:")) const [secondUser, firstAdmin] = swap(admins[0], users[1]) logUser(secondUser) logAdmin(firstAdmin) } function test2() { console.log(chalk.yellow("test2:")) const [secondAdmin, firstUser] = swap(users[0], admins[1]) logAdmin(secondAdmin) logUser(firstUser) }
解答
本题的关键点是 swap
这个方法,它即可以接受Admin
类型为参数,也可以接受 User
类型为参数,并且还需要根据传入参数的顺序把它们倒过来放在数组中放回。
也就是说传入的是 v1: User, v2: Admin
,需要返回 [Admin, User]
类型。
这题就比较有难度了,首先需要用到泛型[3] 来推断出参数类型,并且和结果关联起来:
function swap<T, K>(v1: T, v2: K) { return [v2, v1] }
此时结果没有按照我们预期的被推断成 [K, T]
,而是被推断成了 (K | T)[]
,这是不符合要求的。
这是因为 TypeScript 默认我们数组中的元素是可变的,所以它会「悲观的」推断我们可能会改变元素的顺序。鼠标放到运行函数时的swap
上,我们可以看出类型被推断为了:
function swap<Admin, User>(v1: Admin, v2: User): (Admin | User)[]
要改变这一行为,我们加上 as const
来声明它为常量,严格保证顺序。
function swap<T, K>(v1: T, v2: K) { return [v2, v1] as const }
此时再看看 swap
的推断:
function swap<Admin, User>(v1: Admin, v2: User): readonly [User, Admin]
类型被自动加上了 readonly
,并且也严格的维持了顺序,太棒啦。
总结
先用其中的几道题练练手,到第六题的时候,已经涉及到了一些进阶的用法,非常有挑战性。不知道小伙伴们对于这样的做题形式是否感兴趣,还剩下不少有挑战性的题目,如果反馈不错的话,我会继续更新这个系列。
本文仓库地址: