【2023秋第8节课】解析 TypeScript

【2023秋第8节课】解析 TypeScript

本文编写自26届同学:Ἀφροδίτη

TypeScript

TS简介

  • 是JavaScript的超集,浏览器不能直接识别,需要编译器转换成纯JS语言
  • 强类型 静态 编程语言,更加方便地进行类型检查和代码重构,提高代码的可靠性和可维护性

TS的优劣

优势:

  1. 类型安全:提供强大的类型系统,能够在编译阶段发现类型错误,避免在运行时出现意外的错误。这有助于提高代码的可维护性和可靠性
  2. 代码提示和重构:TypeScript 的类型系统能够为代码编辑器和集成开发环境(IDE)提供丰富的代码提示和重构功能。这使得开发人员能够更方便地编写和理解代码,提高了开发效率
  3. 门槛性:TypeScript 是一个高级开发人员必会的技能之一,同时也是能熟练使用 ts 的竞争力远比只能使用 js 高,尤其是大厂几乎是纯 100% 的使用率,很多开源库也都是 ts 编写,对于阅读源码也有很大的帮助。

劣势:

  1. 限制了 JavaScript 的灵活性
  2. 增加了代码量却没有提高应用程序的性能。增加编码时间却没有提高运行效率,还会徒增编译时间
  3. 虽然 TypeScript 与现有的 JavaScript 库和框架(如 jQuery、React 等)可以无缝集成,但在一些情况下,使用 TypeScript 可能会对这些库的类型定义和用法产生一些限制或不兼容的问题。

开发准备

VSCode插件和配置

插件:

  1. TypeScript Importer 这个插件会收集项目中所有的类型定义,在你敲出 : 时进行补全提示,并且会自动将这个类型导入。
  2. move TS 这个插件的作用就是当你移动了 TypeScript 文件的时候,会自动更新引用导入的路径。比如从home/project/learn-interface.ts 修改成 home/project/interface-notes/interface-extend.ts。
  3. errorLens 改插件会将你的 VS Code 底部问题栏的错误下直接显示到代码文件中的对应位置。
  4. Pretty TypeScript Errors 可以帮助你人性化 TypeScript 的错误提示。

配置:

在设置里面搜索 TypeScript Inlay Hints 匹配出来的就是 ts 提示相关的配置了。推荐开启的有如下几个:

  • Enum Member Values
  • Function Like Return Types
  • Parameter Names
  • Variable Types 具体的信息可以直接在设置中查看。

开发环境

首先我们需要全局安装 TypeScript: npm i typescript -g 接着我们创建一个 index.ts 文件,输入以下内容: const content: string = "hello typescript"; 对于 TS 代码是不能直接在浏览器中运行的,所以我们需要先将其编译成 JS 代码,使其可以被浏览器理解。我们可以通过 tsc index.ts 命令来编译 .ts 文件:

// helloworld.ts => helloworld.js
const content = "hello typescript";

如果我们想要直接在服务端执行 TypeScript 文件,我们需要 ts-node 以及 ts-node-dev 这类工具,它们能直接执行 ts 文件,并且支持监听和文件的重新执行。 首先我们将其安装到全局: npm i ts-node -g 然后创建另一个 ts-demo 的文件夹。 在文件夹下执行命令: tsc --init 这样会初始化 TypeScript 的配置文件:tsconfig.json。 接着我们在该目录下创建一个 index.ts 文件。 console.log("hello typescript"); 再使用 ts-node 来执行: ts-node index.ts 如果顺利的话,就会输出对应的语句了。 ts-node 本身并不支持自动监听文件变化然后重新执行,而这一功能又是刚需,比如用 ts 进行一些服务端的 api 开发。我们可以借助 ts-node-dev 库来实现这一能力。 首先先全局安装: npm i ts-node-dev -g 之后我们可以使用 tsnd 这一简称来执行命令: tsnd --respawn index.ts 其中 –respawn 表示启动监听。 当我们修改文件的时候无需重新运行命令,只要保存了就会自动监听文件的改变并重新执行。 对于一些简单的纯 TS 代码,能检查类型错误,并快速调整 tsconfig,我们可以通过 TS Playground 来进行编写。

TS类型

  • TS 类型包含所有 JS 类型 null、undefined、string、number、boolean、bigInt、Symbol、object(数组,对象,函数,日期),还包含 void、never、enum、unknown、any 以及 自定义的 type 和 interface

变量声明

  • var/let/const 标识符: 数据类型 = 赋值
const data: string = 'hello'  
//变量类型定义时已决定,不能再赋予其他类型的值

类型推导

  • 如果没有明确指定类型,TS 会隐式的推导出一个类型
  • 这类型根据赋值的类型推断,没有赋值则为 any 类型,能自动推导出类型,没必要手动指定
const data = 'hello'
//编译器会自动补全为上个代码块中的式子

基础类型

原始类型

const name: string = 'lanshan';
const age: number = 24;
const male: boolean = false;
const undef: undefined = undefined;
const nul: null = null;
const bigintVar1: bigint = 9007199254740991n;
const bigintVar2: bigint = BigInt(9007199254740991);
const symbolVar: symbol = Symbol('unique');

JS 中的数据类型在 TS 中都有,其中 undefined 和 null 是其他类型的子集,在 strictNullChecks 不为 true 的情况下都可以赋值给其他类型。除此之外,还有 void、never 等类型。 void 类型可以用来表述一个内部没有显式 return 的函数返回值

function func1(): void {}
function func2(): void {
  return;
}

这两个函数都会被隐式推导为 void。 需要注意的是 undefined 能够被赋值给 void 类型的变量,所以下面函数的返回值类型既可以写成 void 也可以写成 undefined。 function func3(): void { return undefined; } function func4(): undefined { return undefined; } 在严格空检查模式(tsconfig.json 中 strictNullChecks 为 true)下, null 和 undefined 值都不属于任何一个类型,它们只能赋值给自己这种类型或者 any (有一个例外,undefined 也可以赋值给 void)。

数组

数组在 JS 中属于对象类型,在 TS 中,我们将其与对象先分开来研究。 在 TS 中有两种方法来声明一个数组类型: // 都是表示数组元素全为字符串的数组 const arr1: string[] = []; const arr2: Array<string> = []; 这两种方法都是等价的,但我们一般是以前者为主。

元组

元组是一种特殊的数组,其长度固定,可以给 TS 提供如数组越界访问等类型报错。如: const arr3: string[] = ['lan', 'shan']; // 显式越界 console.log(arr3[599]); // 隐式越界 const [ele1, ele2, ele3, ...other] = arr3; 对于数组,这种情况 TS 是不会给我们报错的,但是如果我们通过元组来定义: const arr4: [string, string] = ['lan', 'shan']; console.log(arr4[599]); 就会给我们类型提示的的错误:长度为“2”的元组类型“[string, string]”在索引“599“处没有元素。 同时元组也支持在某个位置上的可选成员: const arr5: [string, number?, boolean?] = ['lanshan']; 这时元组的长度类型也会变成也会变为 1 | 2 | 3: type TupleLength = typeof arr5.length; // 1 | 2 | 3 你可能会觉得元组的类型可读性并不好,比如 [string, number, boolean] 你不能直接知道这三个元素都代表什么,还不如用对象的形式。但是我们可以给元组中的元素打上类似属性的标签:

const arr7: [name: string, age: number, male: boolean] = ['lanshan', 599, true];
React的useState就是个元组,类似于
function useState<T>(state: T):[T,(newState: T)=>void]{
  let currentState = State
  const changeState = newState => {
    currentState = newState
  }
  return[currentState,changeState]
}
const [count,setCount]=useState(10)

对象

我们可以通过 interface 声明一个对象结构,命名通常以 I 开头,来表示 Interface:

interface IDescription {
  name: string;
  age: number;
  male: boolean;
}
const obj1: IDescription = {
  name: 'lanshan',
  age: 599,
  male: true,
};

对于普通描述来说,每个属性的值必须一一对应到接口的属性类型,且不能有多的属性,也不能有少的属性。 除此之外,我们还可以对属性进行修饰,常见的修饰包括可选(optional)和只读(readonly)这两种。 如下是可选修饰的示例:

interface IDescription {
  name: string;
  age: number;
  male?: boolean;
  func?: Function;
}
const obj2: IDescription = {
  name: 'lanshan',
  age: 599,
  male: true,
  // 无需实现 func 也是合法的
};

这种情况下 obj2.male 和 obj2.func 会联合 undefined 类型,如 obj2.male 的类型为实际上为 boolean | undefined 表示 boolean 或 undefined 类型都符合。 除了可选修饰,还有只读修饰:

interface IDescription {
  readonly name: string;
  age: number;
}
const obj3: IDescription = {
  name: 'lanshan',
  age: 599,
};
// 无法分配到 "name" ,因为它是只读属性
obj3.name = "蓝山";

在数组和元组中,也可以进行只读修饰,但是只能将整个数组或元组标记为只读,而不能只标记某一项,且一旦标记为只读,那么就不在具有 push、pop 等方法。 除了赋予 Interface 之外,还有 object、Object、{} 这三个类型。 被原型链折磨过的人可能知道原型链的顶端是 Object 的原型,也就意味着所有原始类型和对象类型都会指向 Object,在 TS 中就包含了所有的类型:

const tmp1: Object = undefined;
const tmp2: Object = null;
const tmp3: Object = void;
const tmp4: Object = 'lanshan';
const tmp5: Object = 599;
const tmp6: Object = { name: 'lanshan' };
const tmp7: Object = () => {};
const tmp8: Object = [];

除了 Object 之外,还有 Boolean、Number、String 等几个装箱类型,其同样包含了一些超出预期的类型,如 String 包含对应的拆箱类型 string,以及 undefined、null、void 等,不包括其他装箱类型的拆箱类型。 在任何情况下都不应该使用这些装箱类型。 object 的引入就是为了解决对 Object 装箱类型的错误使用,它代表所有非原始类型的类型,即数组、对象、函数类型等:

const tmp22: object = { name: 'lanshan' };
const tmp23: object = () => {};
const tmp24: object = [];

当你不确定某个变量的具体类型,但能确定它不是原始类型,可以使用 object。但更推荐进一步区分,也就是使用 Record<string, unknown> 或 Record<string, any> 表示对象,unknown[] 或 any[] 表示数组,(…args: any[]) => any 表示函数这样。 最后一个是 {},{} 意味着任何非 null/undefined 的值,使用它和使用 any 同样恶劣。

字面量类型 联合类型

我们先来看一下如下的代码:

interface IRes {
  code: number;
  status: string;
  data: any;
}

在大多数情况下 ,这里的 code 和 status 会来自于一组确定值的集合,比如 code 可能是 1000/1001/1002 中的一个,而 status 可能是 success/failure 中的一个,而上面的类型只给出了一个宽泛的范围,我们不能获取精准的提示的同时,也失去了 TS 类型即文档的功能。 这个时候我们就可以使用字面量类型和联合类型:

interface Res {
  code: 10000 | 10001 | 50000;
  status: "success" | "failure";
  data: any;
}

这个时候我们就可以获得精准的类型推导了。 字面量类型:上面的 “success” 按理说应该是一个值,但是它同时也可以作为类型使用,也就是字面量类型,它代表着比原始类型更精准的类型,同时也是原始类型的子类型。

const str: "lanshan" = "lanshan";
const num: 599 = 599;
const bool: true = true;
const str1: "lanshan" = "lanshan";
const str2: string = "lanshan";

单独使用字面量类型比较少见,通常情况下是和联合类型一起使用,用来表示一组字面量类型。 联合类型:联合类型代表了一组类型的可用集合,只要最终赋值的类型属于联合类型的成员之一,就认为是符合这个联合类型的,联合类型之间的成员通过 | 来连接。 联合类型还有一种使用场景就是通过多个对象的联合来实现手动的互斥属性:

interface Tmp {
  user:
    | {
        vip: true;
        expires: string;
      }
    | {
        vip: false;
        promotion: string;
      };
}

如这段代码,user 中使用联合类型,如果 vip 为 true 的话,那么接下来的类型就会收窄到 vip 允许的类型。

枚举

enum PageUrl {
  Home_Page_Url = "url1",
  Setting_Page_Url = "url2",
  Share_Page_Url = "url3",
}

如果说元组是特殊的数组,那么也可以将枚举理解为特殊的对象,枚举内的这些常量被真正约束在一个命名空间下。 如果你没有声明枚举的值,它会默认使用数字枚举,并从 0 开始,以 1 递增。

enum Items {
  Foo, // 0
  Bar, // 1
  Baz // 2
}

在这个例子中,Items.Foo 、 Items.Bar、 Items.Baz 的值依次是 0,1,2 。 如果你只为一个成员指定了枚举值,之前未赋值成员仍然会使用从 0 递增的方式,之后的成员则会开始从枚举值递增。

enum Items {
  // 0 
  Foo,
  Bar = 599,
  // 600
  Baz
  }

枚举类型也支持延迟求值:

const returnNum = () => 100 + 499;
enum Items {
  Foo = returnNum(),
  Bar = 599,
  Baz
}

枚举和对象还有一个重要的区别:对象是单向映射的,而枚举是双向映射的。枚举既可以从枚举对象映射到枚举值,也可以从枚举值映射到枚举成员。

enum Items {
  Foo,
  Bar,
  Baz
}
const fooValue = Items.Foo; // 0
const fooKey = Items[0]; // "Foo"
// 以上枚举会被编译成如下 JS
"use strict";
var Items;
(function (Items) {
    Items[Items["Foo"] = 0] = "Foo";
    Items[Items["Bar"] = 1] = "Bar";
    Items[Items["Baz"] = 2] = "Baz";
})(Items || (Items = {}));

obj[k] = v 的返回值是 v,因此这里的 obj[obj[k] = v] = k 本质上就是进行了 obj[k] = v 和 obj[v] = k 这样的两次赋值。 只有枚举值为数字的枚举成员才能进行这样的双向枚举,字符串枚举成员仍然只会单向映射。

函数

函数类型就是描述函数入参和函数返回值的类型,下面是一个简单的例子:

function foo1(name: string): number {
  return name.length;
}

在 JS 中,我们称上面的写法为函数声明,除了函数声明之外,我们还可以通过函数表达式来声明一个函数:

const foo2 = function (name: string): number {
  return name.length
}
const foo3: (name: string) => number = function (name) {
  return name.length
}

这里的 foo3 中的 (name: string) => number 是 TS 中的函数类型签名,箭头前表示入参的类型,箭头后表示返回值的类型。 而实际的箭头函数的类型标注是这样的:

const foo4 = (name: string): number => {
  return name.length
}
const foo5: (name: string) => number = (name) => {
  return name.length
}

在函数中我们也可以指定可选参数和默认参数:

// 在函数逻辑中注入可选参数默认值
function foo6(name: string, age?: number): number {
  const inputAge = age || 18; // 或使用 age ?? 18
  return name.length + inputAge
}
// 直接为可选参数声明默认值
function foo7(name: string, age: number = 18): number {
  const inputAge = age;
  return name.length + inputAge
}
也可以在函数中使用 rest 参数:
// 数组接收
function foo8(arg1: string, ...rest: any[]) { }
// 元组接收
function foo9(arg1: string, ...rest: [number,boolean]) { }

下面将介绍一下函数重载的概念。 在复杂逻辑下,函数可以有多组入参类型和返回值类型:

function func(foo: number, bar?: boolean): string | number {  if (bar) {
    return String(foo);
  } else {
    return foo * 599;
  }
}

该函数的作用是当 bar 为 true,返回值为 string 类型,否则为 number 类型。对于这种函数类型,我们不能直观感受到,只能知道这个函数返回值可能是 string 或者 number。 这个时候就需要用到函数重载的概念。将上面例子用函数重载重写了之后为:

function func(foo: number, bar: true): string;
function func(foo: number, bar?: false): number;
function func(foo: number, bar?: boolean): string | number {
  if (bar) {
    return String(foo);
  } else {
    return foo * 599;
  }
}
const res1 = func(599); // number
const res2 = func(599, true); // string
const res3 = func(599, false); // number

any unknown never与类型断言

有些时候 TS 并不需要特别精准的类型控制,比如 conosle.log 方法能够接收任意类型的参数,不管是数组、字符串、对象或者是其他的,统统来着不拒,那难道我们需要把所有的类型全部通过联合类型串联起来? TS 提供了一个 any 内置类型,来表示任意类型。 log(message?: any, ...optionalParams: any[]): void 在这里,一个被标记为 any 类型的参数可以接受任意类型的值。除了 message 是 any 以外,optionalParams 作为一个 rest 参数,也使用 any[] 进行了标记,这就意味着你可以使用任意类型的任意数量类型来调用这个方法。 你可以在 any 类型变量上任意地进行操作,包括赋值、访问、方法调用等等,此时可以认为类型推导与检查是被完全禁用的,它能兼容所有类型,也能够被所有类型兼容。 因为其过于自由,并不推荐使用 any。 如果你是想表达一个未知类型,更合理的方式是使用 unknown。 unknown 类型和 any 类型有些类似,一个 unknown 类型的变量可以再次赋值为任意其它类型,但只能赋值给 any 与 unknown 类型的变量。

let unknownVar: unknown = "lanshan";
unknownVar = false;
const val1: string = unknownVar; // Error
const val2: any = unknownVar;

要对 unknown 类型进行属性访问,需要进行类型断言,类型断言的作用是虽然这是一个未知的类型,但我跟你保证它在这里就是这个类型。

let obj: unknown = {
        sayName: () => {
                console.log("lanshan");
  }
}
obj.sayName() // Error
(obj as { sayName: () => {} }).sayName() // 使用类型断言
const str: string = "lanshan";
(str as any).func().foo().prop;
function foo(union: string | number) {
  if ((union as string).includes("lanshan")) { }
  if ((union as number).toFixed() === '599') { }
}

除了类型断言之外,还有非空断言: 非空断言其实是类型断言的简化,它使用 ! 语法,即 obj!.func()!.prop 的形式标记前面的一个声明一定是非空的(实际上就是剔除了 null 和 undefined 类型),比如这个例子:

declare const foo: {
  func?: () => ({
    prop?: number | null;
  })
};
foo.func().prop.toFixed();

func 在 foo 中不一定存在,prop 在 func 调用结果中不一定存在,在我们确定存在的时候,可以使用非空断言来避免 TS 报错。 了解了 any 和 unknown,而 never 类型,其表示根本不存在的类型。

// 这里的 declare 就相当于是声明一个类型,但是不像声明变量那样会占用内存
declare const strOrNumOrBool: string | number | boolean;
if (typeof strOrNumOrBool === "string") {
  console.log("str!");
} else if (typeof strOrNumOrBool === "number") {
  console.log("num!");
} else if (typeof strOrNumOrBool === "boolean") {
  console.log("bool!");
} else {
  throw new Error(Unknown input type: ${strOrNumOrBool}); // never 类型的 strOrNumOrBool
}

如上面的例子,在之前的判断中,我们将所有类型的情况都考虑到了,如果不满足所有条件的话,就会进入到最后的 else,也就是根本不会存在的类型中,这里就是 never 类型。

类型工具

如果说 TS 中的内置类型就像是最基础的积木,仅仅是拥有积木是不够的,还需要一些类型工具来帮助我们操作这些类型。 按照使用目的来分类可以分为类型创建和类型安全保护两类 。

类型别名

类型别名 type 并不复杂,它可以用来定义一个类型的别名:

type StatusCode = 200 | 301 | 400 | 500 | 502;
type Handler = (name: string) => void;
type Objtype = {
  name: string;
  age: number;
}
// 类型别名也可以引用其他的类型别名,组成更复杂的类型结构
type Name = string;
type Age = number;

type Person = {
 name: Name;
 age: Age;
};

类型别名结合泛型可以帮助我们更好地使用其作为类型工具的作用,对于泛型这里我们只了解其和类型别名相关的使用,可以简单理解为就像函数接收参数一样,该类型也会接收一个类型参数。

type Factory<T> = T | number | string;
const foo: Factory<boolean> = true;
type FactoryWithBool = Factory<boolean>;

对于这里的 T 的命名,并不是固定的,写成 T 只是我们的约定俗成。 可以通过类型别名结合泛型来声明一些有意思的类型工具: type MaybeNull = T | null; // 可能为 null type MaybeArray = T | T[]; // 可能为数组 类型别名与接口(interface)有一定的相似性,但它们之间存在一些差异。类型别名更适合表示联合类型、交叉类型、元组等,而接口更适合表示对象的结构。此外,接口可以被合并(当多个接口具有相同的名称时),而类型别名不具备这个特性,比如:

interface Person {
  name: string;
  age: number;
}
interface Person {
  address: string;
}
const person: Person = {
  name: 'lanshan',
  age: 30,
  address: '123 Main St.',
};

类型别名不具备这个特性,如果你定义了两个具有相同名称的类型别名,它们不会自动合并,而是会产生一个编译时错误。

联合类型与交叉类型

在 TypeScript 中,联合类型 | 和交叉类型 & 是两个重要的概念。 之前我们了解字面量类型的时候已经了解了联合类型 | 了,还有一个和联合类型相似的孪生兄弟:交叉类型 &。 对于联合类型,你只需要满足其中一个类型即可,而对于交叉类型,需要符合里面所有的类型,通常和对象类型一起使用:

interface NameStruct {
  name: string;
}
interface AgeStruct {
  age: number;
}
type ProfileStruct = NameStruct & AgeStruct;
const profile: ProfileStruct = {
  name: "lanshan",
  age: 18
}

如果我们将两个原始类型交叉:

type StrAndNum = string & number; // never

会返回类型 never,因为根本不存在既是 string 又是 number 的类型,而 never 又正好描述了根本不存在的类型。 如果是两个联合类型组成的交叉类型:

type UnionIntersection1 = (1 | 2 | 3) & (1 | 2); // 1 | 2
type UnionIntersection2 = (string | number | symbol) & string; // string

我们只需要实现两边联合类型的交集就行了。

索引类型

索引类型包含索引签名类型、索引类型查询、索引类型访问,其都是通过索引的形式来进行类型操作。

索引签名类型

索引类型签名主要是在接口或对象类型别名中,快速声明一个键值类型一致的类型结构:

interface AllStringTypes {
  [key: string]: string;
}

type AllStringTypes = {
  [key: string]: string;
}

interface Person {
  name: string;
  age: number;
  [key: string]: string | number;
}

索引类型查询

索引类型查询也就是 keyof 操作符,其可以将对象中所有键转换为对应的字面量类型,再组成联合类型。

interface Foo {
  lanshan: 1,
  599: 2
}

type FooKeys = keyof Foo; // "lanshan" | 599

可以通过以下伪代码来进行理解:

type FooKeys = Object.keys(Foo).join(" | ");

索引类型访问

在 JS 中我们可以通过 obj[expression] 的方式来访问一个对象的属性,而在 TS 中,我们也可以通过 type[expression] 的方式来访问一个键所对应的类型。

interface Foo {
  propA: number;
  propB: boolean;
}

type PropAType = Foo['propA']; // number
type PropBType = Foo['propB']; // boolean

将索引类型查询和索引类型访问结合在一起,可以一次性获取这个对象所有的键对应的类型的联合类型:

interface Foo {
  propA: number;
  propB: boolean;
  propC: string;
}

type PropTypeUnion = Foo[keyof Foo]; // string | number | boolean

映射类型

这类类型工具可以将一个类型的属性映射到另一个类型,我们直接来看示例:

type Stringify<T> = {
  [K in keyof T]: string;
};

假设这个工具类型会接受一个对象类型( T 为一个对象类型,也就是我们需要映射的对象),使用 keyof 获得这个对象类型的键名组成字面量联合类型,然后通过映射(即这里的 in 关键字,用来遍历联合类型)将这个联合类型的每一个成员映射出来,并将其键值类型设置为 string。

interface Foo {
  prop1: string;
  prop2: number;
  prop3: boolean;
  prop4: () => void;
}

type StringifiedFoo = Stringify<Foo>;

// 等价于
interface StringifiedFoo {
  prop1: string;
  prop2: string;
  prop3: string;
  prop4: string;
}

理解了上面代码之后,我们可以实现一个 Clone 的类型工具:

type Clone<T> = {
  [K in keyof T]: T[K];
};

类型查询操作符

TS 提供了 typeof 操作符来返回后面参数的类型。

类型守卫

is 关键字 TS 中提供了类型推导能力,其随着你的代码逻辑不断尝试收窄类型,这一能力称之为类型控制流。 可以想象成有一条河流,其从上到下流过程序,随着代码分支出一条条支流,只有特定的类型才能进入对应的支流。

// 这里的 declare 就相当于是声明一个类型,但是不像声明变量那样会占用内存
declare const strOrNumOrBool: string | number | boolean;

if (typeof strOrNumOrBool === "string") {
  // 一定是字符串!
  strOrNumOrBool.charAt(1);
} else if (typeof strOrNumOrBool === "number") {
  // 一定是数字!
  strOrNumOrBool.toFixed();
} else if (typeof strOrNumOrBool === "boolean") {
  // 一定是布尔值!
  strOrNumOrBool === true;
} else {
  // 要是走到这里就说明有问题!不存在的类型用 never
  const _exhaustiveCheck: never = strOrNumOrBool;
  throw new Error(`Unknown input type: ${_exhaustiveCheck}`);
}

我们也可以将类型判断通过 is 关键字抽离到外面:

function isString(input: unknown): input is string {
  return typeof input === "string";
}

function foo(input: string | number) {
  if (isString(input)) {
    // ...
  }
}

下面是一个比较常用的来判断是否属于 Falsy 类型的工具:

export type Falsy = false | "" | 0 | null | undefined;

export const isFalsy = (val: unknown): val is Falsy => !val;

in 关键字 我们可以通过 in 操作符来判断类型是否存在在对象类型中:

interface Foo {
  foo: string;
  fooOnly: boolean;
  shared: number;
}

interface Bar {
  bar: string;
  barOnly: boolean;
  shared: number;
}

function handle(input: Foo | Bar) {
  if ('foo' in input) {
    input.fooOnly;
  } else {
    input.barOnly;
  }
}

泛型

TS 在 React 中的应用

类型体操

类型编程被戏称为类型体操,也就是我们可以对传入的类型参数(泛型)做各种逻辑运算,产生新的类型,这就是类型编程。

infer 类型

首先我们讲一个之前没有提到过的类型:infer,用于在条件类型中推断类型变量。 具体来说,infer 关键字可以用于从一个类型中提取出另一个类型,并将其赋值给一个类型变量。这个类型变量可以条件类型中使用,从而实现类型的推断。 比如我们有一个类型 ReturnType,它可以获取一个函数类型的返回值类型:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

在上面的例子中,我们使用了 infer 关键字来推断函数类型的返回值类型。具体来说,我们使用了条件类型 T extends (…args: any[]) => infer R,它表示如果类型 T 是一个函数类型,则将其返回值类型赋值给类型变量 R。

function add(a: number, b: number): number {
  return a + b;
}

type AddReturnType = ReturnType<typeof add>; // number

我们再看一个例子理解一下:

type First<Tuple extends unknown[]> = Tuple extends [infer T,...infer R] ? T : never;

type res = First<[1,2,3]>; // 1

对于类型体操过于复杂,这里只介绍两种比较简单常用的套路。

模式匹配

我们知道字符串可以和正则表达式做模式匹配,找到匹配的部分并返回。 TS 的类型同样也可以做模式匹配。 比如我们想提取一个如下的 Promise 类型的 value 的类型:

type GetValueType<T> = T extends Promise<infer Value> ? Value : never;

type p = Promise<'lanshan'>;
type pRes = GetValueYype<p>; // 'lanshan'

Typescript 类型的模式匹配是通过 extends 对类型参数做匹配,结果保存到通过 infer 声明的局部类型变量里,如果匹配就能从该局部变量里拿到提取出的类型。 数组中的模式匹配 如果数组想提取第一个元素或者最后一个元素的类型该怎么做?

// 提取第一个
type GetFirst<Arr extends unknown[]> = 
    Arr extends [infer First, ...unknown[]] ? First : never;

// 提取最后一个
type GetLast<Arr extends unknown[]> = 
    Arr extends [...unknown[], infer Last] ? Last : never;
// 去掉最后一个元素
type PopArr<Arr extends unknown[]> = 
    Arr extends [] ? [] 
        : Arr extends [...infer Rest, unknown] ? Rest : never;

// 去掉第一个元素
type ShiftArr<Arr extends unknown[]> = 
    Arr extends [] ? [] 
        : Arr extends [unknown, ...infer Rest] ? Rest : never;

字符串中的模式匹配 判断字符串是否以某个前缀开头:

type StartsWith<Str extends string, Prefix extends string> = 
    Str extends `${Prefix}${string}` ? true : false;

替换字符串的某部分:

type ReplaceStr<
    Str extends string,
    From extends string,
    To extends string
> = Str extends `${infer Prefix}${From}${infer Suffix}` 
        ? `${Prefix}${To}${Suffix}` : Str;

函数中的模式匹配 获取函数参数:

type GetParameters<Func extends Function> = 
    Func extends (...args: infer Args) => unknown ? Args : never;

获取返回值参数:

type GetReturnType<Func extends Function> = 
    Func extends (...args: any[]) => infer ReturnType 
        ? ReturnType : never;

递归复用

TS 中对于两种情况我们需要想到递归,第一种是当数组、字符串长度不确定时,第二种是当对象层数不确定时。 我们先来看一下数组、字符串使用递归的情况。 首先是反转数组:

// 需要我们把下面数组
type arr = [1,2,3,4,5];
// 转化为如下的数组
type arr = [5,4,3,2,1];

如果不用递归我们可能会写成这样:

type ReverseArr<Arr extends unknown[]> = 
    Arr extends [infer One, infer Two, infer Three, infer Four, infer Five]
        ? [Five, Four, Three, Two, One]
        : never;

但是如果数组的长度不确定,上面的方法就失效了,所以我们可以想到每次只处理一个类型,剩下的递归去做,直到满足结束条件:

type ReverseArr<Arr extends unknown[]> = 
    Arr extends [infer First, ...infer Rest] 
        ? [...ReverseArr<Rest>, First] 
        : Arr;

这段代码的逻辑就是每次只提取第一个元素,并放到最后,剩余的元素作为数组又进行之前的操作,结束的情况就是最后数组只剩下一个元素,不满足模式匹配的条件,就会返回 Arr。 如果需要实现在数组中查找是否存在某个元素: // 判断两个元素是否相等

type IsEqual<A, B> = (A extends B ? true : false) & (B extends A ? true : false);

type Includes<Arr extends unknown[], FindItem> = 
    Arr extends [infer First, ...infer Rest]
        ? IsEqual<First, FindItem> extends true
            ? true
            : Includes<Rest, FindItem>
        : false;

从第一个元素开始和查找的元素进行判断,当相同时返回 true,否则用剩下的元素作为数组继续判断。 接下来我们来看字符串,之前我们写了字符串的 replace,但是该类型只能替换第一个找到的部分,不能将该字符串所有的匹配部分给替换掉,我们可以通过递归来改写:

type ReplaceAll<
    Str extends string, 
    From extends string, 
    To extends string
> = Str extends `${infer Left}${From}${infer Right}`
        ? `${Left}${To}${ReplaceAll<Right, From, To>}`
        : Str;

结束条件是不再满足模式匹配,也就是没有要替换的元素,这时就直接返回字符串 Str。 对于对象来说,我们通过对象类型深拷贝来体现。 如果我们想要给一个对象类型所有属性添加上只读特性,可以这样操作:

type ToReadonly<T> =  {
    readonly [Key in keyof T]: T[Key];
}

但这样只能添加到第一层,如果我们传入的对象类型是这样的:

type obj = {
    a: {
        b: {
            c: {
                f: () => 'lan',
                d: {
                    e: {
                        shan: string
                    }
                }
            }
        }
    }
}

那我们就需要用到递归:

type DeepReadonly<Obj extends Record<string, any>> = {
    readonly [Key in keyof Obj]:
        Obj[Key] extends object
            ? Obj[Key] extends Function
                ? Obj[Key] 
                : DeepReadonly<Obj[Key]>
            : Obj[Key]
}

原文链接:https://juejin.cn/post/7314474878475976740 作者:LanShanFE

(0)
上一篇 2023年12月20日 下午4:10
下一篇 2023年12月20日 下午4:22

相关推荐

发表回复

登录后才能评论