多年老鸟教你TypeScript要做的性能优化、关键字进阶与实践、常用几个技巧
前言
大家好,我是易师傅 ,距离上一次的发文还是上次了;最近沉迷游戏,所以懈怠放纵了自我。
好了,不狡辩了,没错,就是变懒了 ~
话不多说,基于自己使用 TypeScript 有了几年的时间,故简单做一个总结,如果你想进阶高级 TSer 以及想如何更好的写好 TS 代码,我觉得这篇文章对你肯定是受益匪浅,终身受用 ~
默认大家了解 TypeScript 基础相关知识且使用了一段时间,所以不会做一些基本介绍,如果需了解的可自行搜索相关文章;
一、TS 的性能优化
为什么 TS 要做性能优化?
同样也是类似的问题在前端框架 Svelte
上再次上演,虽然 Svelte
的作者没有直接想 Deno
那样说的透彻,但是大概意思也是对编译效率等相关问题的解决。
下图为 Svelte 作者个人的回答,使用谷歌翻译:
但是我们真是也要追随大佬们的脚步而选择放弃吗?
这里笔者个人理解一个词概括就是:TypeScript 能用就用
,具体为啥下面也会讲解;
如何做?
1. 优先使用 interface 而非 type
我们都知道,type
与 interface
的作用非常相似
interface Foo { prop: string }
type Bar = { prop: string };
简单一看,其实它俩差异并不大,没错,的确相差不大;
假设我们定义了多个不同的类型,interface
一般可使用 extends
来继承;而 type
需要使用交叉类型 &
来实现集成;那么现在就会展现出来他们的差异来了;
差异一:因为 interface
定义的是一个单一平面对象类型,可以检测属性是否冲突;
差异二:交叉类型只是递归的合并属性,有些情况下会产生 never
;
所以当您可能需要将类型并集或交集时,请使用 interface
2.非特殊情况必须要使用类型注解
一句话解释就是:添加类型注解,尤其是返回类型,可以节省编译器的大量工作;
虽然 TypeScript 的 类型推导 是非常方便的,但是如果在一个大型项目中就会变得较慢,影响开发时间;
这是因为 命名类型(类型注解) 会比 匿名类型(类型推导) 让你的编译器减少了大量的读写声明文件的时间;
3.优先使用基础类型而不是联合类型
使用联合类型的代码:
上述例子中,当我们调用 printSchedule 函数时,每次将参数传递给 printSchedule 函数时,需要比较联合类型里的每个元素;
如果是一两个元素的联合类型那还好,如果是多个呢,这样就引起编译速度的问题;
所以我们需要改进成基础类型代码:
在实际项目开发中,类似的情况几乎都会遇到,例如:
// 定义一个 div 并且声明类型
const div: HTMLElement = document.createElement('div')
其实这样写是没问题的,但是我们看 HTMLElement 类型,他其中就是 HTMLDivElement | HTMLImageElement 等的一个无穷多的一个联合类型;
所以我们只需要改进下代码:
// 定义一个 div 并且声明类型
const div: HTMLDivElement = document.createElement('div')
4. 复杂类型抽离成简单类型
当我们进行复杂类型编程时,会发现很多同学,喜欢把多个运行时写在同一个类型中;
所以为了优化,我们需要抽离,其实这就类似编程的 单一职责模式 ;
但是 TS 的类型抽离不仅仅只是代码变得单一职责了,更多的也是减少了编译器的负担;
上述例子中,当我们每次调用 foo 函数时,TypeScript 都会重新运行条件类型,这样就会增加编译器的负担,所以我们需要优化;
我们可以提取其中的类型为一个新的类型别名,这样这个类型别名就会被编译器缓存了,减少编译器的负担;
5. 控制单个项目大小
为什么笔者在上面说:在实际项目中 TypeScript 是能用则用
,固然在单个大型项目中,TypeScript 往往会增加项目的负担,但是我们换一种思路去想一下这个问题;
如果单个项目没有达到所谓 Deno 等库的体积量呢?那么这时候 TypeScript 是不是利大于弊呢?
所以我们需要:
- 单个项目尽可能的避免大型开发;
- 大型项目尽可能的分成多个项目,参考微服务架构;
- 增加模块尽可能的少,避免项目的负担;
二、关键字进阶与实践
1. keyof 的秘密
keyof 是一个单目运算符,接受一个对象类型作为参数,返回该对象的所有键名组成的联合类型。
interface obj {
a: string
b: number
}
keyof obj // "a" | "b"
上面这是把一个正常的对象类型作为了 keyof 的参数,如果我们用 any 作为 keyof 的参数呢?会怎样?
keyof any // string | number | symbol
这是因为 JavaScript 对象的键名只有三种类型,所以对于任意对象的键名的联合类型就是 string | number | symbol
TypeScript 的内置对象 Record
就是 keyof any
的一个很好的实现例子;
// 内置的对象
type Record<K extends string | number | symbol, T> = { [P in K]: T; }
// 其实就是:
type Record<K extends keyof any, T> = { [P in K]: T; }
需要注意的是有一种特殊情况:
如果在 tsconfig.ts
配置文件中开启了 keyofStringsOnly: true
,那么:
keyof any // string
这也是为什么 Record
写的是 K extends string | number | symbol
问答环节:
- 既然
keyof any = string | number | symbol
,那么keyof unknwon
输出什么呢? - 凭你的第一印象说出你的答案,直接打在评论区,说错也不要紧,因为这对实际项目没什么用;
- 具体原因会在下面讲到;
2. 方括号 [] 的秘密
方括号运算符([]) 用于取出对象的键值类型,比如T[K]会返回对象T的属性K的类型。
type Person = {
age: number;
name: string;
alive: boolean;
};
// Age 的类型是 number
type Age = Person['age'];
其实除了上述的用法,还有多种不常见的使用方法
用法一:方括号运算符的参数也可以是属性名的索引类型。
type Obj = {
[key:string]: number,
};
// string 就是上面 key 的索引类型
type T = Obj[string]; // number
用法二:对于数组而言,可以使用 number
作为方括号的参数,或者使用字符串 'length'
为参数
// MyArray 的类型是 { [key:number]: string }
const MyArray = ['a','b','c'];
// 等同于 (typeof MyArray)[number]
type Person = typeof MyArray[number] // 返回 string
type Len = typeof MyArray['length'] // 返回 number
3. is 的秘密
is 运算符
用来描述返回值属于 true 还是 false
当函数返回布尔值的时候,可以使用 is 运算符,限定返回值与参数之间的关系。
const isString = (val: unknown): val is string => typeof val === 'string';
is 运算符一般用于描述函数的返回值类型,写法一般采用固定 参数名 is Type 的形式,即左侧为当前函数的参数名,右侧为某一种类型。
is 运算符主要用于类型保护:
上图可以看到 str. 后面的属性都是字符串特有的,这就做到了类型保护
的功能;
但是我们细想一想,如果把上面例子中的 val is string
换成 boolean
可以吗?
当然是可以的,只是呢它达不到类型保护的功能;
会发现 str. 后面的属性只有独属于 Boolean 类型的几个属性了;
而且在 Vue3 、Vueuse 等相关开源库中都大量使用到了该用法:Vue3 场景链接
4. as 的秘密
我们常用 as 关键字
来转换一个类型,这样极大的方便了我们的编程,毕竟遇事不决就直接 as any
搞定,要是还搞不定,那就双重断言 as any as any
, 简直不要太完美;
但是 as 其实还有更高级的用法
用法一:键名重映射
type A = {
foo: number;
bar: number;
};
type B = {
[p in keyof A as `${p}ID`]: number;
};
// 等同于
type B = {
fooID: number;
barID: number;
};
在例子中,类型 B 是类型 A 的映射,但在映射时把属性名改掉了,在原始属性名后面加上了字符串 ID。
这就叫做键名重映射
用法二:属性的过滤
type User = {
name: string,
age: number
}
type Filter<T> = {
[K in keyof T as T[K] extends string ? K : never]: string
}
type FilteredUser = Filter<User> // { name: string }
在例子中,映射 K in keyof T
获取类型 T 的每一个属性以后,然后使用 as Type
修改键名。
它的键名重映射 as T[K] extends string ? K : never
,使用了条件运算符。
意思就是如果属性值 T[K] 的类型是字符串,那么属性名不变,否则属性名类型改为 never,即这个属性名不存在。
这样就等于过滤了不符合条件的属性,只保留属性值为字符串的属性。
由于键名重映射可以修改键名类型,所以原始键名的类型不必是 string | number | symbol
,任意的联合类型都可以用来进行键名重映射。
5. 其它关键字总结归纳
5.1. in
JavaScript 语言中,in 运算符
用来确定对象是否包含某个属性名。
TypeScript 语言的类型运算中,in 运算符
有不同的用法,用来取出(遍历)联合类型的每一个成员类型。
type U = 'a'|'b'|'c';
type Foo = {
[Prop in U]: number;
};
// 等同于
type Foo = {
a: number,
b: number,
c: number
};
5.2 extends
条件运算符 TypeA extends TypeB ? A : B
可以根据当前类型是否符合某种条件,返回不同的类型。
T extends U ? X : Y
// true
type T = 1 extends number ? true : false;
5.3 infer
infer 关键字
用来定义泛型里面推断出来的类型参数,也就相当于声明一个变量,而不是外部传入的类型参数。
它通常跟条件运算符一起使用,用在 extends 关键字
后面的父类型之中(其中父类型包括函数类型等多种类型集合)。
type Flatten<Type> =
Type extends Array<infer Item> ? Item : Type;
5.4 模板字符串
TypeScript 允许使用模板字符串,构建类型;
模板字符串的最大特点,就是内部可以引用其他类型;
type World = "world";
// "hello world"
type Greeting = `hello ${World}`
注意:模板字符串可以引用的类型一共6种,分别是 string、number、bigint、boolean、null、undefined
引用这6种以外的类型会报错。
5.5 typeof 运算符
在 JavaScript 语言中,typeof
运算符是一个一元运算符,返回一个字符串,代表操作数的类型。
TypeScript 将typeof
运算符移植到了类型运算,它的操作数依然是一个值,但是返回的不是字符串,而是该值的 TypeScript 类型
。
const a = { x: 0 };
const b = { y: 0 } as const;
type T0 = typeof a; // { x: number }
type T1 = typeof a.x; // number
type T2 = typeof b; // { readonly y: 0; }
注意点:
- JavaScript 的
typeof
和 TypeScript 的typeof
使用时切勿混淆,它们的一个重要区别在于,编译后,前者会保留,后者会被全部删除。 - TypeScript 的
typeof
类型时,只能用在类型运算之中(即跟类型相关的代码之中),不能用在值运算。 - TypeScript 的
typeof
命令的参数不能是类型。
三、TS 常用的几个使用技巧
1. Type Guards 类型保护
function isNumber(value: any): value is number {
return typeof value === "number";
}
const validateAge = (age: any) => {
if (isNumber(age)) {
// 操作数字 age
} else {
console.error("The age must be a number");
}
};
也就是上面讲的 is 关键词的使用;
使用语法就是:参数 is 类型
2. Immutable Types 不可变类型(const assertions)
也称之为 const 断言;
使用语法: as const
const ErrorMessages = {
InvalidEmail: "Invalid email",
InvalidPassword: "Invalid password",
// ...
} as const;
// 将会报错
ErrorMessages.InvalidEmail = "New error message";
以上类型将会被推导成为不可变类型,如图所示:
as const
会让 TypeScript 将 ErrorMessages 对象中的属性标记为只读(readonly)。
这意味着,你不能对这些属性进行修改。
此外,as const
还会让 TypeScript 为每个属性推断出一个更精确的类型,即它们的字面量类型,而不是一般的字符串类型。
所以,ErrorMessages 的类型会被推断为:
{
readonly InvalidEmail: "Invalid email",
readonly InvalidPassword: "Invalid password",
// ...
}
注意:as const
不会将上下文的表达式(对象类型)转换为不可变类型
let arr = [1, 2, 3, 4];
let foo = {
name: "foo",
contents: arr,
} as const;
foo.name = "bar"; // 报错!
foo.contents = []; // 报错!
foo.contents.push(5); // 这将可以正常运行!
我们如果将它转换为不可变类型呢?
let foo = {
name: "foo",
contents: [1, 2, 3, 4],
} as const;
foo.contents.push(5); // 报错 Property 'push' does not exist on type 'readonly [1, 2, 3, 4]'
3. satisfies 代替 as
在 TypeScript4.9 中新引入了 satisfies 操作符
,所以使用时请注意当前 TypeScript 版本;
当我们希望确保某些 TypeScript 的表达式与某些 TypeScript 的类型匹配,又希望以该表达式的类型来推理的时候,satisfies 操作符
就应运而生了。
1. 场景一:
type Colors = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number];
const palette: Record<Colors, string | RGB> = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255]
};
// 调用数组的 at 方法
const redComponent = palette.red.at(0);
// 调用 string 的 toUpperCase 方法,此处会报错:Property 'toUpperCase' does not exist on type 'string | RGB'.
const greenNormalized = palette.green.toUpperCase();
上例 palette
的类型为 Record<Colors, string | RGB>
,那么当我们调用 palette.green.toUpperCase()
会报错;
主要是因为 RGB
类型上没有 toUpperCase()
方法,所以就会报错;
那可能有同学会问, 咱们 palette.green
的值就是一个字符串啊,为啥还会报错?
虽然我们都知道他是一个字符串,但是 TypeScript 不知道啊!
所以这个就需要我们反向推导 palette.green
的类型;
用 as 关键字
可以吗,答案是肯定不可以的,as 关键字
和上面的 类型注解
是一样的用法,也会报相同的错;
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255]
} as Record<Colors, string | RGB>
// 调用 string 的方法,此处会报错:Property 'toUpperCase' does not exist on type 'string | RGB'.
const greenNormalized = palette.green.toUpperCase();
在以前还真没办法解决,在 TypeScript4.9 版本中新引入了一个关键字 satisfies 操作符
;
所以我们只需要改成:
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255]
} satisfies Record<Colors, string | RGB>
// 正常运行:调用数组的 at 方法
const redComponent = palette.red.at(0);
// 正常运行:调用 string 的方法
const greenNormalized = palette.green.toUpperCase();
虽然我们知道 palette 属性值
的类型为 联合类型 string | RGB
;
但是我们的 satisfies 关键字
反向推导出了现在 palette.green
的类型是一个 string
,所以现在调用 toUpperCase()
是没有任何问题的;
2. 场景二:
上例中是一个正常的 TypeScript 写法,其中 a 变量虽然有三个值
,但是咱们的编辑器是 没法推导出来属性 link 的
;
因为 interface MyElement
中可以无限随便追加属性值的写法 [key: string]: any
;
那么我们需要怎么做呢?
是的,就是这样简单;
所以大多数情况下 satisfies
是可以代替 as
使用的,但是 as
是无法代替 satisfies
;
4. any VS unknown VS never
- any:
- 这个就不多讲了,它就是 AnyScript 的重要组成部分。
- unknown:
- 不能直接赋值给其他类型的变量(除了any 类型和 unknown 类型);
- 不能直接调用 unknown 类型变量的方法和属性;
- unknown 类型变量能够进行的运算是有限的,只能进行比较运算(运算符==、===、!=、!==、||、&&、?)、取反运算(运算符!)、typeof 运算符和 instanceof 运算符这几种,其他运算都会报错;
- 所以 keyof unknwon 返回的 never;
- never:
- never 类型的使用场景,主要是在一些类型运算之中,保证类型运算的完整性;
- never 类型可以赋值给任意其他类型呢?这也跟集合论有关,空集是任何集合的子集。TypeScript 就相应规定,任何类型都包含了never 类型。因此,never 类型是任何其他类型所共有的,TypeScript 把这种情况称为**“底层类型”(bottom type)**。
- 一句话概括就是 TypeScript 有两个“顶层类型”(any 和 unknown),但是“底层类型”只有 never 唯一一个。
也就是说:
任意类型 extends any
都是成立的,any extends 非 unknown 任意类型
都是不成立的;
type TestAnyUnknown = unknown extends any ? true : false // true
type TestAnyString = string extends any ? true : false // true
type TestUnknownAny = any extends unknown ? true : false // true
任意类型 extends unknown
都是成立的,unknown extends 非any任意类型
都是不成立的;
type TestUnknown = any extends unknown ? true : false // true
type TestStringUnknown = string extends unknown ? true : false // true
type TestUnknownString = unknown extends string ? true : false // false
never extends 任意类型
都是成立的,任意类型 extends never
都是不成立的;
type TestNever = never extends string ? true : false // true
type TestToNever = unknown extends never ? true : false // false
5. Record<string, any> 代替 object
Record
是内置的一个类型工具,也是一个较为常用的工具
在一般实际项目中,很多同学喜欢声明一个对象类型时,喜欢这么写:
interface Person {
[key: string]: unknown
}
const Human: Person = {
name: "Steve",
age: 42
}
虽然这是在 TS 中对象类型的一种有效解决方案,但它在编写复杂类型时较为复杂,而且局限性也大;
例如,想使用一些特定的键值:
type AllowedKeys = 'name' | 'age';
interface Person {
[key: AllowedKeys]: unknown
// error: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
}
const Human: Person = {
name: "Steve",
age: 42
}
这样是会报错的,那么我们应该怎么做呢?
type AllowedKeys = 'name' | 'age';
type Person = Record<AllowedKeys, unknown>;
const Human: Person = {
name: "Steve",
age: 42
}
至此,相关技巧已总结完毕;
如果您有更好的使用技巧,欢迎补充,笔者会添加对应的技巧以及署名!!!
实战(兴趣参加)
讲了再多,也没有实际动手来得快,为了巩固加深大家的印象;
我们一起来动手实现一个实现 ArrayToObject,把一个数组中的值,返回并转成一个对象,实现后可把链接展示在评论区
const ParamsArray = ["a=1","b=2","c=3"]
// 提示:涉及到的知识点:infer、keyof、in、[]、extends、typeof、as const、模版字符串、Record<>
type ArrayToObject<T extends readonly string[] = []> = ?
type ObjectValue = ArrayToObject<typeof ParamsArray>
// 返回以下对象
// type ObjectValue = {
// a: "1";
// b: "2";
// c: "3";
// }
最后
感谢大家的支持,码字实在不易,其中如若有错误,望指出,如果您觉得文章不错,记得 点赞关注加收藏
哦 ~
转载自:https://juejin.cn/post/7272229204870660154