TypeScript本身更像是一个工具,并没有很大的技术含量在里面值得讨论,值得讨论的是,如何高效地善用这个工具罢了。
究其根本,TypeScript(TS)的就是一个关于 Type 的工具,在实践中,我们为数据写好与之对应的数据结构就可以了,之后一系列的事,TypeScript 都会帮我们做好,因此,相对于编程来说,TS真是再简单不过了。
简单在于在框架加持下,我们的脚手架都已经配置好了 TS 相关配置,我们只要写写 interface 和 type就好了,再加上一些联合类型以及交叉类型,对了,泛型也不能少,这就足够可以满足大多数应用场景了,因此,TS 是不难的,即便没有强类型语言基础的人也不会很难,因为一切需要我们手动编写的 Type 皆可测。
只有人心难测,因为即便难测的 Type 也有 never 可以兜底处理。
人人都会写 type,因此我想谈的不是怎么写 type,是如何在实践中高效地写 type。
1.善用内置类型和类型推断
-
善用 typeof,keyof 这两个类型操作符, 如果对于已经有数据结构的变量,可就别老老实实一笔一划写 type,一个 typeof 直接搞定不香吗?keyof 同理,对于已经定义好 key 的变量,直接获取就完善了,没必要搞个联合类型或者枚举类型了,这两个类型操作符可以说是我在日常用的频率最高的了。
-
一定不要小看内置类型,内置类型又有那么多,哪些重要呢?我先随便举两个例子吧 例子1:在后台管理系中,如果我们在用户列表中定义用户的类型,我们可能会像下面这样定义
export interface UserListItem {
id:number
userId: number
email: string
mobile: string
description: string
status: number
sex: number
}
定义好以后,我们固然可以读取到每一行的user 属性,但是当我们更改的时候,我们可能只需要向后台传部分数据,如修改状态,就只需要传 id、status 这两个字段,难道我们要单独定义一个type UpdateUserStatus = {id:number,status:number}这样的类型吗?事实上是不必要的,TS 为了减少定义重复子结构类型所带来的心智负担,已经为我们准备了诸多内置类型,这里我们只需要用到 Patrial 这个类型转换一下就好了。
Patrial:构造一个所有属性都Type设置为optional的类型。该实用程序将返回一个表示给定类型的所有子集的类型。
如上转换一下就好了,这些属性就都变成了可选属性,TS 又只检查接口对应需要的属性,而不会检查多余的属性,因此一下子就达到了类型转换的功能,与之功能相反的类型是 Require
Required<Type>
构造一个类型,该类型由Typeset的所有属性设置为required。与之相反Partial。
例子
interface Props {
a?: number;
b?: string;
}
const obj: Props = { a: 5 };
const obj2: Required<Props> = { a: 5 };
Property 'b' is missing in type '{ a: number; }' but required in type 'Required<Props>’.
顾名思义,这个类型能把接口上所有 optionnal 的属性转换成必须,诸如此类的类型还有很多
- Record<Keys,Type> 构造一个对象类型,其属性键为Keys,属性值为Type。该实用程序可用于将一个类型的属性映射到另一个类型。
interface CatInfo {
age: number;
breed: string;
}
type CatName = "miffy" | "boris" | "mordred";
const cats: Record<CatName, CatInfo> = {
miffy: { age: 10, breed: "Persian" },
boris: { age: 5, breed: "Maine Coon" },
mordred: { age: 16, breed: "British Shorthair" },
};
cats.boris;
// ^ = const cats: Record
- Pick<Type, Keys> 通过Keys从中选择属性集来构造类型Type。
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
todo;
// ^ = const todo: TodoPreview
- Omit<Type, Keys> 通过从中选择所有属性Type,然后移除来构造一个类型Keys。
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Omit<Todo, "description">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
todo;
// ^ = const todo: TodoPreview
除了以上这些,还有 Extract,Exclude 等等这些高频的工具类型,以帮助我们更加高效转换类型,更多详细关于 Utility Types => Utility Types 手册
2.学会使用 infer
如果说 TS 有难点,那么到目前为止,我认为 infer 的用法差不多可以说是最难的,如果可以很熟练的使用 infer,我认为 TS 水平一定不会差到哪里,相应的,如果我是面试官,我要用一道题来考察一个人的 TS 水平怎么样,那么毫无疑问是考察infer了,下面就先介绍一下 infer。
- 条件类型 因为infer 只能在条件类型中使用,首先先介绍一下条件类型(类似三目运算符)。 直接上例子
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
type Example1 = Dog extends Animal ? number : string;
// ^ = type Example1 = number
type Example2 = RegExp extends Animal ? number : string;
// ^ = type Example2 = string
条件类型采用某种形式,看起来类似于condition ? trueExpression : falseExpressionJavaScript中的条件表达式
SomeType extends OtherType ? TrueType : FalseType;
仔细看上面几个例子应该就明白条件类型是怎么回事了吧,如果还没看懂,那就再仔细一点看,但凡是懂三目运算符的都看得明白,只不过 condition 这个判断表达式的形式永远是 A 类型 extends B 类型,因为TS 好像目前只有这个操作符有 true or false 判断,它和我们程序中的 a===b、b==c、'abc'==='abcd'这些表达式都是一样的,只不过把具体变量的值换成了操作 type,判断 typeA 是否是 TypeB 的子类,形如 TypeA extends TypeB,这就是条件类型,举个例子
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
type EmailMessageContents = MessageOf<Email>;
// ^ = type EmailMessageContents = string
type DogMessageContents = MessageOf<Dog>;
// ^ = type DogMessageContents = never
- infer 说完条件类型,再看来看看我们的 infer。
通常,条件类型的检查会为我们提供一些新信息。就像使用类型保护缩小范围可以为我们提供更特定的类型一样,条件类型的真实分支将通过我们检查的类型进一步约束泛型
让我们handlebook这个例子
type MessageOf<T> = T["message"];
Type '"message"' cannot be used to index type 'T'.
在此示例中,TypeScript错误,因为T不知道其名为的属性message。我们可以约束T,而TypeScript将不再报错:
type MessageOf<T extends { message: unknown }> = T["message"];
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
type EmailMessageContents = MessageOf<Email>;
// ^ = type EmailMessageContents = string
再进一步(先啰嗦的直接跳到 infer 那一步,哈哈哈,我直接从handlebook 搬过来的,这一段)
但是,如果我们想MessageOf采用任何类型,并且默认为类似never某个message属性不可用的情况该怎么办?我们可以通过将约束移出并引入条件类型来做到这一点:
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
type EmailMessageContents = MessageOf<Email>;
// ^ = type EmailMessageContents = string
type DogMessageContents = MessageOf<Dog>;
// ^ = type DogMessageContents = never
我们只是发现自己使用条件类型来应用约束,然后提取出类型。这最终成为一种常见的操作,使得条件类型使操作变得更容易。 条件类型为我们提供了一种使用infer关键字从真实分支中进行比较的类型推断的方法。例如
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
我们的内置类型 ReturnType便是用 infer 实现,下面用 infer 实现一遍
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
? Return
: never;
type Num = GetReturnType<() => number>;
// ^ = type Num = number
type Str = GetReturnType<(x: string) => string>;
// ^ = type Str = string
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
// ^ = type Bools = boolean[]
一句话,infer 就是将条件分支里的类型推导了出来,并且可以多次使用条件类型以达到复杂的操作,不知道你们懂了没有,下面多用几个例子说明一下
// 自己定义的 parameters,前置知识:MyParameters
type MyParameters<T extends (...args: any[]) => any> = T extends (
...args: infer U
) => any
? U
: any;
const fun3 = (hello: 3, acttion: 'kkk') => {};
type Fun3 = (name: number, acttion: string) => any;
type P7 = Parameters<Fun3>;
let p7: P7 = [12, '23'];
// P7 = [name:number,acttion:string]
可以嵌套条件类型以形成一系列按顺序求值的模式匹配:
type Unpacked<T> = T extends (infer U)[]
? U
: T extends (...args: any[]) => infer U
? U
: T extends Promise<infer U>
? U
: T;
type T0 = Unpacked<string>;
// ^ = type T0 = string
type T1 = Unpacked<string[]>;
// ^ = type T1 = string
type T2 = Unpacked<() => string>;
// ^ = type T2 = string
type T3 = Unpacked<Promise<string>>;
// ^ = type T3 = string
type T4 = Unpacked<Promise<string>[]>;
// ^ = type T4 = Promise
type T5 = Unpacked<Unpacked<Promise<string>[]>>;
// ^ = type T5 = string尝试
lodash的 get,第一眼看上去,的确有些吃力
type PropType<T, Path extends string> = string extends Path
? unknown
: Path extends keyof T
? T[Path]
: Path extends `${infer K}.${infer R}`
? K extends keyof T
? PropType<T[K], R>
: unknown
: unknown;
declare function get<T, P extends string>(obj: T, path: P): PropType<T, P>;
const obj = { a: { b: { c: 42, d: 'hello' } } };
const value = get(obj, 'a.b.d');
// value:string
const value1 = get(obj, 'a.b.c');
// value1:number
关于 infer 更多资料 => www.typescriptlang.org/docs/handbo…
3.泛型、never,unknown,字符串模版类型
-
泛型 为了更高效的编写 type,泛型一定是必不可少的,可以让我们少定义很多重复结构的类型,这个应该就很简单了,就不详细说了。
-
never
简单来说就是永远不会发生的类型,对我而言,可以做兜底的错误处理,特别是分支处理中。
interface Foo {
type: 'foo';
}
interface Bar {
type: 'bar';
}
type All = Foo | Bar;
function handleValue(val: All) {
switch (val.type) {
case 'foo':
// 这里 val 被收窄为 Foo
break;
case 'bar':
// val 在这里是 Bar
break;
default:
// val 在这里是 never
const exhaustiveCheck = val;
console.log(exhaustiveCheck);
break;
}
}
- unknown
顶层类型,我可以通过收缩类型来确定类型,以继续操作,如果不想使用 any 以致丢失类型的话。
- 字符串模版类型
使得拼错字符串的类型有救了,如 vuex 的子module下的 mutations 和 actions,也因为如此,本来 vuex5才完全支持 TS 的似乎要提到 vuex4.1版本了,可以关注一下。
后面这些说的很简单,但真要用得恰到好处,需要自己去实践中大量摸索。 type 只是工具,但是用得不好,反而带来了心智负担,希望我们都能高效编写 type,善用工具,毕竟,Lift is short.