typescript:类型工具(上)

一、类型别名

(1)类型别名的作用主要是对一组类型或一个特定类型结构进行封装,以便于在其它地方进行复用。

//抽离一组联合类型
type StatusCode = 200 | 301 | 400 | 500 | 502;
type PossibleDataTypes = string | number | (() => unknown);

const status: StatusCode = 502;

//抽离一个函数类型
type Handler = (e: Event) => void;

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

//声明一个对象类型,就像接口那样
type ObjType = {
  name: string;
  age: number;
}

 (2)类型别名还能作为工具类型。工具类同样基于类型别名,只是多了个泛型

(3)在类型别名中,类型别名可以这么声明自己能够接受泛型(我称之为泛型坑位)。一旦接受了泛型,我们就叫它工具类型。

type Factory<T> = T | number | string;

1、虽然现在类型别名摇身一变成了工具类型,但它的基本功能仍然是创建类型,只不过工具类型能够接受泛型参数,实现更灵活的类型创建功能

const foo: Factory<boolean> = true;

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

type FactoryWithBool = Factory<boolean>;

const foo: FactoryWithBool = true;

 3、同时,泛型参数的名称(上面的 T )也不是固定的。通常我们使用大写的 T / K / U / V / M / O ...这种形式。如果为了可读性考虑,我们也可以写成大驼峰形式(即在驼峰命名的基础上,首字母也大写)的名称,比如:

type Factory<NewType> = NewType | number | string;

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

type MaybeNull<T> = T | null;

这个工具类型会接受一个类型,并返回一个包括 null 的联合类型。这样一来,在实际使用时就可以确保你处理了可能为空值的属性读取与方法调用:

type MaybeNull<T> = T | null;

function process(input: MaybeNull<{ handler: () => {} }>) {
  input?.handler();
}

 类似的还有 MaybePromise、MaybeArray。这也是我在日常开发中最常使用的一类工具类型:

type MaybeArray<T> = T | T[];

// 函数泛型我们会在后面了解~
function ensureArray<T>(input: MaybeArray<T>): T[] {
  return Array.isArray(input) ? input : [input];
}

 5、总之,对于工具类型来说,它的主要意义是基于传入的泛型进行各种类型操作,得到一个新的类型。

6、如果按照使用方式来划分,类型工具可以分成三类:操作符、关键字与专用语法。而按照使用目的来划分,类型工具可以分为 类型创建 与 类型安全保护 两类。(这一节我们将学习的类型工具就属于类型创建,它们的作用都是基于已有的类型创建新的类型,这些类型工具包括类型别名、交叉类型、索引类型与映射类型。)

二、联合类型与交叉类型

(1)交叉类型的符号是&,即按位与运算符。需要符合这里的所有类型,才可以说实现了这个交叉类型,即 A & B需要同时满足 A 与 B 两个类型才行。

interface NameStruct {
  name: string;
}

interface AgeStruct {
  age: number;
}

type ProfileStruct = NameStruct & AgeStruct;

const profile: ProfileStruct = {
  name: "linbudu",
  age: 18
}

很明显这里的 profile 对象需要同时符合这两个对象的结构。从另外一个角度来看,ProfileStruct 其实就是一个新的,同时包含 NameStruct 和 AgeStruct 两个接口所有属性的类型。

(2)交叉类型对于原始类型

type StrAndNum = string & number; // never

我们可以看到,它竟然变成 never 了!看起来很奇怪,但想想我们前面给出的定义,新的类型会同时符合交叉类型的所有成员,存在既是 string 又是 number 的类型吗?当然不。实际上,这也是 never 这一 BottomType 的实际意义之一,描述根本不存在的类型嘛。

(3)对于对象类型的交叉类型,其内部的同名属性类型同样会按照交叉类型进行合并。

type Struct1 = {
  primitiveProp: string;
  objectProp: {
    name: string;
  }
}

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

type Composed = Struct1 & Struct2;

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

(4) 如果是两个联合类型组成的交叉类型呢?其实还是类似的思路,既然只需要实现一个联合类型成员就能认为是实现了这个联合类型,那么各实现两边联合类型中的一个就行了,也就是两边联合类型的交集

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

 (5)总结一下交叉类型和联合类型的区别就是,联合类型只需要符合成员之一即可(||),而交叉类型需要严格符合每一位成员(&&)。

三、索引类型

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

  • 索引签名类型

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

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

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

 1、这时,即使你还没声明具体的属性,对于这些类型结构的属性访问也将全部被视为 string 类型。

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

type PropType1 = AllStringTypes['linbudu']; // string
type PropType2 = AllStringTypes['599']; // string

2、在这个例子中我们声明的键的类型为 string([key: string]),这也意味着在实现这个类型结构的变量中只能声明字符串类型的键。

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

const foo: AllStringTypes = {
  "linbudu": "599"
}

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

const foo: AllStringTypes = {
  "linbudu": "599",
  599: "linbudu",
  [Symbol("ddd")]: 'symbol',
}

 4、索引签名类型也可以和具体的键值对类型声明并存,但这时这些具体的键值类型也需要符合索引签名类型的声明。

interface AllStringTypes {
  // 类型“number”的属性“propA”不能赋给“string”索引类型“boolean”。
  propA: number;
  [key: string]: boolean;
}

这里的符合即指子类型,因此自然也包括联合类型:

interface StringOrBooleanTypes {
  propA: number;
  propB: boolean;
  [key: string]: number | boolean;
}

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

interface AnyTypeHere {
  [key: string]: any;
}

const foo: AnyTypeHere['linbudu'] = 'any value';
  •  索引类型查询

(1) 索引类型查询,也就是 keyof 操作符。严谨地说,它可以将对象中的所有键转换为对应字面量类型,然后再组合成联合类型。注意,这里并不会将数字类型的键名转换为字符串类型字面量,而是仍然保持为数字类型字面量

interface Foo {
  linbudu: 1,
  599: 2
}

type FooKeys = keyof Foo; // "linbudu" | 599
// 在 VS Code 中悬浮鼠标只能看到 'keyof Foo'
// 看不到其中的实际值,你可以这么做:
type FooKeys = keyof Foo & {}; // "linbudu" | 599

 (2)除了应用在已知的对象类型结构上以外,你还可以直接 keyof any 来生产一个联合类型,它会由所有可用作对象键值的类型组成:string | number | symbol。也就是说,它是由无数字面量类型组成的,由此我们可以知道, keyof 的产物必定是一个联合类型

  • 索引类型访问 

interface NumberRecord {
  [key: string]: number;
}

type PropType = NumberRecord[string]; // number

 (1)这里,我们使用 string 这个类型来访问 NumberRecord。由于其内部声明了数字类型的索引签名,这里访问到的结果即是 number 类型。注意,其访问方式与返回值均是类型

(2)更直观的例子是通过字面量类型来进行索引类型访问:

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

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

看起来这里就是普通的值访问,但实际上这里的'propA''propB'都是字符串字面量类型而不是一个 JavaScript 字符串值。索引类型查询的本质其实就是,通过键的字面量类型('propA')访问这个键对应的键值类型(number

 (3)keyof 操作符

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

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

使用字面量联合类型进行索引类型访问时,其结果就是将联合类型每个分支对应的类型进行访问后的结果,重新组装成联合类型。索引类型查询、索引类型访问通常会和映射类型一起搭配使用,前两者负责访问键,而映射类型在其基础上访问键值类型。

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

interface Foo {
  propA: number;
}

// 类型“Foo”没有匹配的类型“string”的索引签名。
type PropAType = Foo[string]; 

 四、映射类型:类型编程的第一步

 (1)映射类型的主要作用即是基于键名映射到键值类型

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

这个工具类型会接受一个对象类型(假设我们只会这么用),使用 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;
}

 (2)改进:既然拿到了键,那键值类型其实也能拿到。

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

这里的T[K]其实就是上面说到的索引类型访问,我们使用键的字面量类型访问到了键值的类型,这里就相当于克隆了一个接口。需要注意的是,这里其实只有K in 属于映射类型的语法,keyof T 属于 keyof 操作符,[K in keyof T]的[]属于索引签名类型,T[K]属于索引类型访问。

 五、总结

我们认识了类型工具中的类型别名、联合类型、索引类型以及映射类型。这些工具代表了类型工具中用于创建新类型的部分,但它们实现创建的方式却五花八门,以下这张表格概括了它们的实现方式与常见搭配。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值