TS 进阶 - 类型工具

2023-05-17 20:22:16 浏览数 (2)

# 类型创建

# 类型别名

代码语言:javascript复制
type A = string;

类型别名主要用于对一组类型或一个特定类型结构进行封装,以便于在其他地方进行复用。

抽离一组联合类型:

代码语言:javascript复制
type StatusCode = 200 | 301 | 400 | 500;
type PossibleDataTypes = string | number | (() => unknown);

const status: StatusCode = 200;
const data: PossibleDataTypes = 123;

抽离一个函数类型:

代码语言:javascript复制
type Handler = (e: Event) => void;

const clickHandler: Handler = (e) => {};

声明对象类型,像接口一样:

代码语言:javascript复制
type ObjectType = {
  name: string,
  age: number,
}

类型别名除了用于进行特定类型的抽离封装,还能作为工具类型。工具类同样基于类型别名,只是多了个泛型

在类型别名中,类型别名可以声明自己能接受泛型,一旦接受了泛型,就称他为工具类型:

代码语言:javascript复制
type Factory<T> = T | number | string;

虽然变成了工具类型,但其基本能力仍然是创建类型,只不过工具类型能够接受泛型参数,实现更灵活的类型创建功能。

可以把工具类型理解为一个函数,泛型是入参,内部逻辑是基于传入参数进行某些操作,返回一个新的类型:

代码语言:javascript复制
type Factory<T> = T | number | string;

const foo: Factory<boolean> = true; // boolean | number | string

一般不会直接使用工具类型来做类型标注,而是再声明一个新的类型别名:

代码语言:javascript复制
type Factory<T> = T | number | string;
type FactoryWithBoolean = Factory<boolean>;

const foo: FactoryWithBoolean = true; // boolean | number | string

泛型参数的名称 T 也不是固定的,通常使用 T/K/U/V/M/O 等。如果为了可读性,也可以使用大驼峰形式:

代码语言:javascript复制
type Factory<NewType> = NewType | number | string;

声明一个简单、有意义的工具类型:

代码语言:javascript复制
// 接受一个类型,返回一个包括 null 的联合类型
type MaybeNull<T> = T | null;

// 可以确保处理了可能为空值的属性读取和方法调用
function process(input: MaybeNull<{ handler: () => {} }>) {
  input?.handler();
}

类似的工具类:

代码语言:javascript复制
type MaybeArray<T> = T | T[];

function ensureArray<T>(input: MaybeArray<T>): T[] {
  if (Array.isArray(input)) {
    return input;
  }
  return [input];
}

工具类型的主要意义是基于传入的泛型进行各种类型操作,得到一个新的类型。

# 联合类型与交叉类型

交叉类型,符号 &,即按位与运算符。正如联合类型的 |,它代表了按位或,即只需要符合联合类型中的一个类型即可认为实现了这个联合类型,如 A | B 只需要实现 AB 即可。而代表按位与的 & ,则需要符合所有类型,才可以说实现了这个交叉类型,即 A & B 需要同时满足 AB 两个类型

代码语言:javascript复制
interface NameStruct {
  name: string;
}

interface AgeStruct {
  age: number;
}

type ProfileStruct = NameStruct & AgeStruct;

const profile: ProfileStruct = {
  name: 'Cell',
  age: 18,
};

对于原始类型,交叉之后的类型不存在:

代码语言:javascript复制
type StringOrNumber = string | number;

type StringAndNumber = string & number; // never

类型的交叉类型,其内部同名属性类型同样会按照交叉类型进行合并:

代码语言:javascript复制
type Struct1 = {
  primitiveProp: string;
  objectProp: {
    name: string;
  };
}

type Struct2 = {
  primitiveProp: number;
  objectProp: {
    age: number;
  };
}

type Struct3 = Struct1 & Struct2;

type PrimitivePropType = Struct3['primitiveProp']; // string & number => never
type ObjectPropType = Struct3['objectProp']; // { name: string } & { age: number } => { name: string; age: number }

如果是两个联合类型组成的交叉类型:

代码语言:javascript复制
type UnionIntersection1 = (1 | 2 | 3) & (1 | 2); // 1 | 2
type UnionIntersection2 = (1 | 2 | 3) & (4 | 5); // never
type UnionIntersection3 = (string | number | symbol) & string; // string

交叉类型和联合类型的区别,联合类型只需要符合成员之一即可,交叉类型需要严格符合每一位成员。

# 索引类型

索引类型指的不是某一特定的类型工具,它其实包含三个部分:索引签名类型索引类型查询索引类型访问。它们都通过索引的形式来进行类型操作,但索引签名类型是声明,后两者都是读取

# 索引签名类型

索引签名类型主要指在接口或类型别名中,通过以下语法快速声明一个键值类型一直的类型结构

代码语言:javascript复制
interface AllStringTypes {
  [key: string]: string;
}

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

即使没有声明具体的属性,对于这些类型结构的属性访问将被视为 string 类型:

代码语言:javascript复制
interface AllStringTypes {
  [key: string]: string;
}

type PropType1 = AllStringTypes['foo']; // string
type PropType2 = AllStringTypes['bar']; // string

注意,声明的键的类型为 string,意味着在实现这个类型结构的变量中只能声明字符串类型的键

代码语言:javascript复制
interface AllStringTypes {
  [key: string]: string;
}

const foo: AllStringTypes = {
  'foo': 'foo',
};

但由于 JavaScript 中,对于 obj[prop] 形式的访问会将数字索引转换为字符串索引访问,即 obj[2022]obj['2022'] 效果一致。因此,在字符串索引签名类型中仍然可以声明数字类型的键。类似的,symbol 类型也是如此。

代码语言:javascript复制
const foo: AllStringTypes = {
  'foo': 'foo',
  2022: '2022',
  [Symbol('foo')]: 'symbol',
};

索引签名类型也可以和具体的键值对类型声明并存,但是必须满足具体的键值类型也符合索引签名类型的声明:

代码语言:javascript复制
interface AllStringTypes {
  foo: string;
  [key: string]: string;
}

// 也包括联合类型
interface StringOrBooleanTypes {
  propA: number;
  propB: boolean;
  [key: string]: string | boolean;
}

索引签名类型常见场景是在重构 JavaScript 代码时,为内部属性较多的对象声明一个 any 的索引类型签名,以此来暂时支持对类型未明确属性的访问,并在后续中逐渐补全类型。

# 索引类型查询

keyof,可以将对象中的所有键转换为对应字面量类型,然后在组合成联合类型。**这里不会将数字类型的键名转换为字符串类型字面量,而是仍然保持数字类型字面量”:

代码语言:javascript复制
interface Foo {
  bar: 1,
  2022: 2,
}

type FooKeys = keyof Foo; // 'bar' | 2022

除了应用于已知的对象类型结构上之外,可以直接 keyof any 来产生一个联合类型——由所有可用作对象键值的类型组成:string | number | symbol

keyof 的产物必定是一个联合类型。

# 索引类型访问

在 JavaScript 中可以通过 obj[expression] 方式来动态访问一个对象属性(即计算属性),expression 表达式会先被执行,然后使用返回值来访问属性。在 TypeScript 中,也可以使用类似方式,但是 expression 需要换成类型:

代码语言:javascript复制
interface NumberRecord {
  [key: string]: number;
}

type PropType = NumberRecord[string]; // number

// 更直观的例子
interface Foo {
  propA: number;
  propB: boolean;
}

// 'propA' 和 'propB' 都是字符串字面量类型,而不是字符串值
type PropAType = Foo['propA']; // number
type PropBType = Foo['propB']; // boolean

索引类型查询的本质就是,通过键的字面量类型(propA)访问这个键对应的键值类型(number)。

可以使用 keyof 一次性获取这个对象所有的键的字面量类型:

代码语言:javascript复制
interface Foo {
  propA: number;
  propB: boolean;
  propC: string;
}

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

使用字面量联合类型进行索引类型访问,其结果就是将联合类型每个分支对应的类型进行访问后的结果,重新组装成联合类型。

注意,在未声明索引签名类型的情况下,不能使用 NumberRecord[string] 这种原始类型的访问方式,而只能通过键名的字面量类型来进行访问。

代码语言:javascript复制
interface Foo {
  propA: number;
}

type PropAType = Foo['propA']; // number
// type PropAType2 = Foo[string]; // error 
// Type 'Foo' has no matching index signature for type 'string'.

# 映射类型

映射类型指的是一个确切的类型工具,主要作用即是基于键名映射到键值类型

代码语言:javascript复制
type Stringify<T> = {
  [K in keyof T]: string
};

这个工具类型接受一个对象类型,使用 keyof 获得对象类型的键名组成字面量联合类型,然后通过映射类型(in 关键字)将这个联合类型的每一个成员映射出来,并将其键值类型设置为 string

代码语言:javascript复制
interface Foo {
  prop1: string;
  prop2: number;
  prop3: boolean;
  prop4: () => void;
}

type StringifiedFoo = Stringify<Foo>;

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

既然拿到了键,那键值类型也可以拿到:

代码语言:javascript复制
type Clone<T> = {
  [K in keyof T]: T[K];
};

interface Foo {
  prop1: string;
  prop2: number;
}

type ClonedFoo = Clone<Foo>;

// 等价于
// interface ClonedFoo {
//   prop1: string;
//   prop2: number;
// }

T[K] 即索引类型访问,K in 属于映射类型语法,keyof T 属于 keyof 操作符,[K in keyof T] 整体 [*] 属于索引签名类型,T[K] 属于索引类型访问。

类型工具

创建新类型的方式

常见搭配

类型别名

将一组类型/类型结构封装,作为一个新的类型

联合类型、映射类型

工具类型

在类型别名的基础上,基于泛型去动态创建类型

使用类型工具

联合类型

创建一组类型集合,满足其中一个类型即满足这个联合类型(|)

类型别名、工具类型

交叉类型

创建一组类型集合,满足其中所有类型才满足映射联合类型(&)

类型别名、工具类型

索引签名类型

声明一个拥有任意属性,键值类型一致的接口结构

映射类型

索引类型查询

从一个接口结构,创建一个由其键名字符串字面量组成的联合类型

映射类型

索引类型访问

从一个接口结构,使用键名字符串字面量访问到对应的键值类型

类型别名、映射类型

映射类型

从一个联合类型依次映射到其内部的每一个类型

工具类型

# 类型安全保护

# 类型查询

TypeScript 存在两种功能不同的 typeof 操作符,常见的是 JavaScript 中用于检查变量类型的 typeof,它会返回 'string' / 'number' / 'object' / 'undefined' 等值。在 TypeScript 中,还新增了用于类型查询的 typeof 操作符,它会返回一个 TypeScript 类型:

代码语言:javascript复制
const str = 'Cell';
const obj = { name: 'Cell' };
const nullVar = null;
const undefinedVar = undefined;
const func = (input: string) => {
  return input.length > 10;
};

type Str = typeof str; // string
type Obj = typeof obj; // { name: string }
type Null = typeof nullVar; // null
type Undefined = typeof undefinedVar; // undefined
type Func = typeof func; // (input: string) => boolean

不仅可以直接在类型标注中使用 typeof,还能在工具类型中使用 typeof

代码语言:javascript复制
const func = (input: string) => {
  return input.length > 10;
};

const func2: typeof func = (name: string) => {
  return name === 'Cell';
};

大部分情况下,typeof 返回的类型就是鼠标悬浮在变量名上时出现的推导后的类型,并且是最窄的推导程度(即到字面量类型的级别)

# 类型守卫

TypeScript 提供了非常强大的类型推导能力,会随代码逻辑不断尝试收窄类型,这种能力称为类型的控制流分析

TypeScript 引入了 is 关键字来显式地提供类型信息:

代码语言:javascript复制
function isString(input: unknown): input is string {
  return typeof input === 'string';
}

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

isString 函数称为类型守卫,在它的返回值中,不在使用 boolean 作为类型标注,而是使用 input is string

  • input 是函数的某个参数
  • is stringis 预期类型,如果这个函数成功返回 ture,那么 is 前参数的类型,就会被这个类型守卫调用方后续的类型控制流分析收集到

注意,类型守卫函数中并不会对判断逻辑和实际类型的关联进行检查,会信任开发者的指定:

代码语言:javascript复制
function isString(input: unknown): input is number {
  return typeof input === 'string';
}

function foo(input: string | number) {
  if (isString(input)) {
    (input).length; // error: number 没有 length 属性
  } else {
    // input: number
  }
}

可以在类型守卫中使用对象类型、联合类型:

代码语言:javascript复制
type Falsy = false | 0 | '' | null | undefined;

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

type Primitive = string | number | boolean | undefined;

const isPrimitive = (val: unknown): val is Primitive => {
  return ['string', 'number', 'boolean', 'undefined'].includes(typeof val);
}

# 基于 in 与 instanceof 的类型保护

in 是 JavaScript 中已有的部分,可以通过 key in object 来判断 key 是否存在于 object 或其原型链上。

在 TypeScript 中,in 也可以用于类型保护:

代码语言:javascript复制
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; // ok
  } else {
    input.barOnly; // ok
  }
}

各个类型独有的数学可以作为可辨识属性,存在具有区分能力的辨识属性称为可辨识联合类型

可辨识属性可以使结构层面的,如 结构 A 的属性 prop 是数组,而 结构 B 的属性 prop 是对象,这样就可以通过 prop 的类型来区分 结构 A结构 B。甚至可以是共同属性的字面量类型差异:

代码语言:javascript复制
function ensureArray(input: number | number[]): number[] {
  if (Array.isArray(input)) {
    return input;
  } else {
    return [input];
  }
}

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

interface Bar {
  kind: 'bar';
  diffType: number;
  barOnly: boolean;
  shared: number;
}

// 对于同名但不同类型的属性,需要使用字面量类型来区分
function handle(input: Foo | Bar) {
  if (input.kind === 'foo') {
    input.fooOnly; // ok
  } else {
    input.barOnly; // ok
  }
}

instanceof 在 JavaScript 中用于判断原型级别的关系,TypeScript 中也可以用于类型保护:

代码语言:javascript复制
class FooBase {}

class BarBase {}

class Foo extends FooBase {
  fooOnly() {}
}

class Bar extends BarBase {
  barOnly() {}
}

function handle(input: FooBase | BarBase) {
  if (input instanceof Foo) {
    input.fooOnly(); // ok
  } else {
    input.barOnly(); // ok
  }
}

# 类型断言守卫
代码语言:javascript复制
import assert from 'assert';

let name: any = 'Cell';

assert(typeof name === 'number'); // throw error

name.toFixed();

断言守卫和类型守卫最大的不同在于,在判断条件不通过时,断言守卫需要抛出一个错误,类型守卫只需要剔除掉预期的类型。

0 人点赞