多年老鸟教你TypeScript要做的性能优化、关键字进阶与实践、常用几个技巧

作者:易师傅github

声明:文章为稀土掘金技术社区首发文章,未获授权禁止转载,侵权必究!

友情提示:阅读此文需 5-10min,如果您很讨厌 TypeScript,请谨慎观看,谢谢您的支持~

前言

大家好,我是易师傅 ,距离上一次的发文还是上次了;最近沉迷游戏,所以懈怠放纵了自我。

好了,不狡辩了,没错,就是变懒了 ~

话不多说,基于自己使用 TypeScript 有了几年的时间,故简单做一个总结,如果你想进阶高级 TSer 以及想如何更好的写好 TS 代码,我觉得这篇文章对你肯定是受益匪浅,终身受用 ~

默认大家了解 TypeScript 基础相关知识且使用了一段时间,所以不会做一些基本介绍,如果需了解的可自行搜索相关文章;

一、TS 的性能优化

为什么 TS 要做性能优化?

我相信同学们应该对 Deno 很熟悉,不知道大家是否还记得 2020 年的一则新闻《Deno 将停用 TypeScript 的五个原因》,其中最重要的莫过于 TypeScript 的编译速度;

同样也是类似的问题在前端框架 Svelte 上再次上演,虽然 Svelte 的作者没有直接想 Deno 那样说的透彻,但是大概意思也是对编译效率等相关问题的解决。

下图为 Svelte 作者个人的回答,使用谷歌翻译:

但是我们真是也要追随大佬们的脚步而选择放弃吗?

这里笔者个人理解一个词概括就是:TypeScript 能用就用,具体为啥下面也会讲解;

如何做?

1. 优先使用 interface 而非 type

我们都知道,typeinterface 的作用非常相似

typescript 复制代码
interface Foo { prop: string }

type Bar = { prop: string };

简单一看,其实它俩差异并不大,没错,的确相差不大;

假设我们定义了多个不同的类型,interface 一般可使用 extends 来继承;而 type 需要使用交叉类型 & 来实现集成;那么现在就会展现出来他们的差异来了;

差异一 :因为 interface 定义的是一个单一平面对象类型,可以检测属性是否冲突;

差异二 :交叉类型只是递归的合并属性,有些情况下会产生 never

所以当您可能需要将类型并集或交集时,请使用 interface

2.非特殊情况必须要使用类型注解

一句话解释就是:添加类型注解,尤其是返回类型,可以节省编译器的大量工作;

虽然 TypeScript 的 类型推导 是非常方便的,但是如果在一个大型项目中就会变得较慢,影响开发时间;

这是因为 命名类型(类型注解) 会比 匿名类型(类型推导) 让你的编译器减少了大量的读写声明文件的时间;

3.优先使用基础类型而不是联合类型

使用联合类型的代码:

上述例子中,当我们调用 printSchedule 函数时,每次将参数传递给 printSchedule 函数时,需要比较联合类型里的每个元素;

如果是一两个元素的联合类型那还好,如果是多个呢,这样就引起编译速度的问题;

所以我们需要改进成基础类型代码:

在实际项目开发中,类似的情况几乎都会遇到,例如:

typescript 复制代码
// 定义一个 div 并且声明类型
const div: HTMLElement = document.createElement('div')

其实这样写是没问题的,但是我们看 HTMLElement 类型,他其中就是 HTMLDivElement | HTMLImageElement 等的一个无穷多的一个联合类型;

所以我们只需要改进下代码:

typescript 复制代码
// 定义一个 div 并且声明类型
const div: HTMLDivElement = document.createElement('div')

4. 复杂类型抽离成简单类型

当我们进行复杂类型编程时,会发现很多同学,喜欢把多个运行时写在同一个类型中;

所以为了优化,我们需要抽离,其实这就类似编程的 单一职责模式

但是 TS 的类型抽离不仅仅只是代码变得单一职责了,更多的也是减少了编译器的负担

上述例子中,当我们每次调用 foo 函数时,TypeScript 都会重新运行条件类型,这样就会增加编译器的负担,所以我们需要优化;

我们可以提取其中的类型为一个新的类型别名,这样这个类型别名就会被编译器缓存了,减少编译器的负担

5. 控制单个项目大小

为什么笔者在上面说:在实际项目中 TypeScript 是能用则用,固然在单个大型项目中,TypeScript 往往会增加项目的负担,但是我们换一种思路去想一下这个问题;

如果单个项目没有达到所谓 Deno 等库的体积量呢?那么这时候 TypeScript 是不是利大于弊呢?

所以我们需要:

  • 单个项目尽可能的避免大型开发;
  • 大型项目尽可能的分成多个项目,参考微服务架构;
  • 增加模块尽可能的少,避免项目的负担;

二、关键字进阶与实践

1. keyof 的秘密

keyof 是一个单目运算符,接受一个对象类型作为参数,返回该对象的所有键名组成的联合类型。

typescript 复制代码
interface obj {
    a: string
    b: number
}

keyof obj // "a" | "b"

上面这是把一个正常的对象类型作为了 keyof 的参数,如果我们用 any 作为 keyof 的参数呢?会怎样?

typescript 复制代码
keyof any // string | number | symbol

这是因为 JavaScript 对象的键名只有三种类型 ,所以对于任意对象的键名的联合类型就是 string | number | symbol

TypeScript 的内置对象 Record 就是 keyof any 的一个很好的实现例子;

typescript 复制代码
// 内置的对象
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 ,那么:

typescript 复制代码
keyof any // string

这也是为什么 Record 写的是 K extends string | number | symbol

问答环节:

  • 既然 keyof any = string | number | symbol ,那么 keyof unknwon 输出什么呢?
  • 凭你的第一印象说出你的答案,直接打在评论区,说错也不要紧,因为这对实际项目没什么用;
  • 具体原因会在下面讲到;

2. 方括号 [] 的秘密

方括号运算符([]) 用于取出对象的键值类型,比如T[K]会返回对象T的属性K的类型。

typescript 复制代码
type Person = {
  age: number;
  name: string;
  alive: boolean;
};

// Age 的类型是 number
type Age = Person['age'];

其实除了上述的用法,还有多种不常见的使用方法

用法一:方括号运算符的参数也可以是属性名的索引类型。

typescript 复制代码
type Obj = {
  [key:string]: number,
};

// string 就是上面 key 的索引类型
type T = Obj[string]; // number

用法二 :对于数组而言,可以使用 number 作为方括号的参数,或者使用字符串 'length' 为参数

typescript 复制代码
// 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 运算符,限定返回值与参数之间的关系。

typescript 复制代码
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 其实还有更高级的用法

用法一:键名重映射

typescript 复制代码
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。

这就叫做键名重映射

用法二:属性的过滤

typescript 复制代码
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 运算符有不同的用法,用来取出(遍历)联合类型的每一个成员类型。

typescript 复制代码
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 可以根据当前类型是否符合某种条件,返回不同的类型。

typescript 复制代码
T extends U ? X : Y

// true
type T = 1 extends number ? true : false;

5.3 infer

infer 关键字用来定义泛型里面推断出来的类型参数,也就相当于声明一个变量,而不是外部传入的类型参数。

它通常跟条件运算符一起使用,用在 extends 关键字后面的父类型之中(其中父类型包括函数类型等多种类型集合)。

typescript 复制代码
type Flatten<Type> =
  Type extends Array<infer Item> ? Item : Type;

5.4 模板字符串

TypeScript 允许使用模板字符串,构建类型;

模板字符串的最大特点,就是内部可以引用其他类型;

typescript 复制代码
type World = "world";

// "hello world"
type Greeting = `hello ${World}`

注意:模板字符串可以引用的类型一共6种,分别是 string、number、bigint、boolean、null、undefined引用这6种以外的类型会报错。

三、TS 常用的几个使用技巧

1. Type Guards 类型保护

typescript 复制代码
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

typescript 复制代码
const ErrorMessages = {
  InvalidEmail: "Invalid email",
  InvalidPassword: "Invalid password",
  // ...
} as const;

// 将会报错
ErrorMessages.InvalidEmail = "New error message";

以上类型将会被推导成为不可变类型,如图所示:

as const 会让 TypeScript 将 ErrorMessages 对象中的属性标记为只读(readonly)。

这意味着,你不能对这些属性进行修改。

此外,as const 还会让 TypeScript 为每个属性推断出一个更精确的类型,即它们的字面量类型,而不是一般的字符串类型。

所以,ErrorMessages 的类型会被推断为:

typescript 复制代码
{
  readonly InvalidEmail: "Invalid email",
  readonly InvalidPassword: "Invalid password",
  // ...
}

注意:as const 不会将上下文的表达式(对象类型)转换为不可变类型

typescript 复制代码
let arr = [1, 2, 3, 4];
let foo = {
  name: "foo",
  contents: arr,
} as const;

foo.name = "bar"; // 报错!
foo.contents = []; // 报错!

foo.contents.push(5); // 这将可以正常运行!

我们如果将它转换为不可变类型呢?

typescript 复制代码
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. 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 任意类型都是不成立的;
typescript 复制代码
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任意类型 都是不成立的;
typescript 复制代码
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 都是不成立的;
typescript 复制代码
type TestNever = never extends string ? true : false // true
type TestToNever = unknown extends never ? true : false // false

4. Record<string, any> 代替 object

Record 是内置的一个类型工具,也是一个较为常用的工具

在一般实际项目中,很多同学喜欢声明一个对象类型时,喜欢这么写:

typescript 复制代码
interface Person {
  [key: string]: unknown
}

const Human: Person = {
  name: "Steve",
  age: 42
}

虽然这是在 TS 中对象类型的一种有效解决方案,但它在编写复杂类型时较为复杂,而且局限性也大;

例如,想使用一些特定的键值:

typescript 复制代码
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
}

这样是会报错的,那么我们应该怎么做呢?

typescript 复制代码
type AllowedKeys = 'name' | 'age';

type Person = Record<AllowedKeys, unknown>;

const Human: Person = {
  name: "Steve",
  age: 42
}

至此,相关技巧已总结完毕;

如果您有更好的使用技巧,欢迎补充,笔者会添加对应的技巧以及署名!!!

实战(兴趣参加)

讲了再多,也没有实际动手来得快,为了巩固加深大家的印象;

我们一起来动手实现一个实现 ArrayToObject,把一个数组中的值,返回并转成一个对象,实现后可把链接展示在评论区

typescript 复制代码
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";
// }

最后

如果想跟我一起讨论技术吹水摸鱼 , 欢迎加入前端学习群聊(群人数太多,只能加vx,望谅解) vx: JeddyGong

感谢大家的支持,码字实在不易,其中如若有错误,望指出,如果您觉得文章不错,记得 点赞关注加收藏 哦 ~

关注我,带您一起搞前端基建 ~

相关推荐
小声读源码13 分钟前
【技巧】dify前端源代码修改第一弹-增加tab页
前端·pnpm·next.js·dify
假客套22 分钟前
2025 后端自学UNIAPP【项目实战:旅游项目】7、景点详情页面【完结】
前端·uni-app·旅游
程序员小张丶35 分钟前
基于React Native开发HarmonyOS 5.0主题应用技术方案
javascript·react native·react.js·主题·harmonyos5.0
Captaincc37 分钟前
Ilya 现身多大毕业演讲:AI 会完成我们能做的一切
前端·ai编程
teeeeeeemo1 小时前
Vue数据响应式原理解析
前端·javascript·vue.js·笔记·前端框架·vue
Sahas10191 小时前
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ is not explicitly defined.
前端·javascript·vue.js
Jinxiansen02111 小时前
Vue 3 实战:【加强版】公司通知推送(WebSocket + token 校验 + 心跳机制)
前端·javascript·vue.js·websocket·typescript
MrSkye1 小时前
React入门:组件化思想?数据驱动?
前端·react.js·面试
BillKu1 小时前
Java解析前端传来的Unix时间戳
java·前端·unix
@Mr_LiuYang1 小时前
网页版便签应用开发:HTML5本地存储与拖拽交互实践
前端·交互·html5·html5便签应用