typescript进阶(一)类型注解

685 阅读10分钟

学好ts的三步:

  1. 基本的类型注解
  2. 深入语法,会封装工具类型或者说类型体操
  3. 工程化配置

本文用于记录、总结学好ts的第一步:基本的类型注解,文章中有什么问题也欢迎大佬指正。

一:基础类型

Object object {}

  • Object为原型链的底层,是所有对象、函数(也是对象)的父级。装箱对象例如Number是number的父级,所以Object类型包含了所有类型。
  • 正因为Object的特性,专门提供了object类型,它表示数组、对象、函数类型。
  • {}可以理解为new Object()实例,所以它跟Object的类型范围一致。

string number boolean symbol

undefined null

  • 在ts中是有意义的类型,你不去声明不会平白无故出现undefined null的。
  • 例如 interface aa { name?: string } 从js的惯性思维理解,name这里相当于name: string | undefined,而实际上上述类型的效果应该为{} | {name: string}。

void

  • 表示函数没有显示返回值。
  • 如果函数显示return但没有值,那么函数的返回值类型应该精确为undefined。

对象类型

  • 使用interface定义对象对外的接口,也可以理解为描述对象的结构
  • 接口的合并,interface 声明的合并,不能覆盖、只能拓展
  • 接口的继承,如果子接口中有父接口中同名属性,那么子接口中的同名属性,那么就需要:子属 兼容 父属
  • 操作修饰符readonly

数组类型

  • 获取数组长度arrType['length'];访问数据类型对应位置的类型attType[0];
  • 元组:类型与位置强关联

字面量类型

枚举enum

  • 枚举的原理
enum Color {
  black,
  red,
  blue,
}
var items;
(function (items) {
  items[items['black' = 0]] = 'black';
  items[items['red' = 1]] = 'red'
  items[items['blue' = 2]] = 'blue'
})(items || (items = {}))
  • 与普通对象的区别
  1. 双向映射
  2. 值需要为数字才会双向映射
  • 常量枚举:编译后啥也没有

函数、class

  • 主要需要考虑的是 入参、结果类型
  • 函数重载,定义顺序最好和实现的逻辑顺序一致
  • 协变与逆变,参数类型逆变,结果类型 协变。一个函数类型的子类型,它的参数可以更少更精简,而且结果类型必须要满足我之前的类型
  • class
  1. 属性和函数的访问符(public protect private)、操作符(readonly)
  2. 抽象类abstract ,抽象类的属性、方法如果需要实现的,也需要abstract 修饰。需要通过其他类来实现这个抽象类,class Other implements Absxx。抽象类的本质就是描述类的结构,也就是和interface一样的作用,所以使用接口也可以替代抽象类的功能,不过语义就没有抽象类好

二:any unkown never 类型断言

  • any:当一个变量类型为any时,这个变量的操作就跳过了类型推断与类型校验。在兼容性判断时,例如 a extends b, 如果泛型a为any,那可以理解为a 既可以是b,也可以不是b
  • unknown:top type类型
  • never: 虚无类型,或者说正常不应该到达的类型
  • 类型断言as
  1. 如果两个类型不存在公共父类型,那么需要双重断言
  2. 非空断言!,例如obj.fn!() 意思是:!前面的声明一定是非空(排除undefined null 类型)。非空断言其实就是as断言的简写,所有断言都是ts类型层面,如果js运行环境中的值与断言的类型不一样,可能就报错

三:类型(的)工具

类型创建

  • type:定义一组类型便于复用,也叫类型别名,不可以重复定义。
  • 联合类型 type C = a | b; 既可以是a也可以是b。
  • 交叉类型,type c = a && b; 那么c的类型就又要满足a,又要满足b;如果a和b都是联合类型,那么就相当于取交集。
  • 索引类型,快速声明一个键值类型一致的结构,{ [name: 类型]: 类型b }
  • 索引类型查询 keyof 类型,得到一个键的联合类型
  • 索引的类型访问,type1 [keyType]。这里的keyType可以是单个字面量类型,也可是该对象类型键类型的集合,下面的例子:
  1. 已知属性的对象类型,objType['a' | 'b']
  2. 索引签名类型,objType[string]
  3. 工具类型,type PickValue = T[keyof T]
  • 映射类型in, 索引签名的好搭档,将一个联合类型当作索引签名类型中的键类型

类型查询

  • typeof,它可以推导出变量的类型,并且是最窄的推导程度。
  • ReturnType, 返回函数类型的返回值类型
type ReturnType1<T extends (...args: any) => any> = T extends (
  ...args: any) => infer R
  ? R
  : never;

类型安全保护

  • 理解:ts会随着你的代码逻辑,不断尝试收窄联合类型,不过这个能力还不够强,只有部分场景。首先需要一个判断环境,if 或者switch case,其次是特性的js判断逻辑:
  1. typeof,变量的类型是由原始类型组成的联合类型,例如string | number。if( type foo === 'string' ) {}。
  2. in,变量的类型是由对象类型组成的联合类型,可以通过判断键是不是独享属性来收窄类型,
  3. intanceof
  4. 对于同名属性但值是不同类型,只能通过字面量来区分
interface Foo1 {
  kind: "foo";
  diffType: string;
  fooOnly: boolean;
  shared: number;
}

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

function handle1(input: Foo1 | Bar) {
  if (input.kind === "foo") {
    input.fooOnly;
  } else {
    input.barOnly;
  }
}

5. ts是无法跨越上下文来进行类型收集,所以如果将判断逻辑提取到其他函数内,那么还要结合is关键字,也就是类型守卫。例如function isString(target: unkown) target is string { return 当返回值为true时,target是string类型}

四:泛型

  • 如果说 TypeScript 是一门对类型进行编程的语言,那么泛型就是这门语言里的(函数)参数

  • 泛型约束extends 默认值。 A extends B 意味着 A 是 B 的子类型,也就是说 A 比 B 的类型更精确,或者说更复杂。

  1. 联合类型子集均为联合类型的子类型,即 1、 1 | 2 是 1 | 2 | 3 | 4 的子类型。
  2. 字面量类型是对应原始类型的子类型,即 'linbudu' extends string,599 extends number 成立。
  3. { name: string } 是 {} 的子类型,因为在 {} 的基础上增加了额外的类型,基类与派生类(父类与子类)同理。
  4. 泛型约束和条件类型,type c<T extends string | number> = T extends string ? 'string' : T extends number ? 'number' : never。
  5. 存在泛型约束和条件类型两个 extends 可能会让你感到疑惑,但它们产生作用的时机完全不同,泛型约束要求你传入符合结构的类型参数,相当于参数校验。而条件类型使用类型参数进行条件判断(就像 if else),相当于实际内部逻辑。
  • 多个泛型参数存在关联
type ProcessInput<
  Input,
  SecondInput extends Input = Input,
  ThirdInput extends Input = SecondInput
> = number;
  • 类型别名中的泛型,主要是通过手动传入
  • 函数中的泛型,会自动将类型 提取给泛型,可以给全部,function handle(input: T): T {} ;也可以给部分 function handle(input: { name: T }): T {}
function universalAdd<T extends number | 
  bigint | string>(x: T, y: T): T {
    return x + (y as any);
}
universalAdd(1, 2) // 1 | 2
universalAdd(1, 'a')// 类型不允许

函数的泛型可以通过主动传入,更多的是通过函数隐式推导出来的,universalAdd这个例子,泛型参数T可以是number string bigint的子类型,当传入第一个参数数字字面量1时,就限定了类型T的原始类型为number,所以第二个参数只能时数字,而类型T又要同时满足1 和 2,所以最终泛型T被推导为1 | 2

  • class中的泛型和函数基本类似

五:类型系统

  • 结构化类型系统,基于类型结构判断兼容性,和名字无关。ts就属于结构化类型系统
  • 标称类型系统,基于类型名称判断兼容性

六:兼容性判断

  • 判断方式
  1. 条件类型来判断,type Result = 'pandu' extends string ? 1 : 2;
  2. 赋值,如果变量 a = b成功 ;那么变量b的类型是变量a的子类型,换种说法 b兼容a
  • 通过条件类型判断,可以直接使用参数去判断,也可以使用泛型。
  1. type a = 类型参数 extends 判断条件的类型 ? true : false
  2. type a< T> = T extends 判断条件的类型 ? true : false
  • 条件类型与infer
  1. 只有条件类型中,才可以使用infer提取部分类型信息
  2. 如果限制了类型参数,在使用infer的时候,可能会丢失原来被限制过的类型信息
type ReverseKeyValue<T extends Record<string, string>> = 
  T extends Record<infer K,infer V>
    ? Record<V, K>
    : never;
// 类型“V”不满足约束“string | number | symbol”
    
// 上面的反转key和value是整个转的,下面是更具体的版本
type ReverseKeyValue<T extends Record<string, string>> =
  T extends Record<infer K, infer V>
  ? {[name in T[keyof T]]: PickKeyByValue<T, name & string>}
  : never;

type PickKeyByValue<T extends Record<string, string>, V extends string> = {
  [name in keyof T]: T[name] extends V ? name : never
}[keyof T]
type obj = {
  name: 'nameV';
  age: 'ageV';
}
type rObj = ReverseKeyValue<obj>;// {nameV: "name"; ageV: "age";}

上面的例子中,因为typeScript 中这样对键值类型进行 infer 推导,将导致类型信息丢失,而不满足索引签名类型只允许 string | number | symbol 的要求。

  1. 约束infer
type FirstType<T> = T  extends [infer F extends string, 
    ...reset: unknown[] ] ? F : never
  • 分布式条件类型:条件类型在满足一定情况下会执行的逻辑
  1. 条件:类型参数通过泛型传入 & 泛型参数是联合类型 & 泛型参数在条件类型中完全裸露(也就是直接作为判断参数)。如type test < T> = T extends 条件类型 ? 'y' : 'n'。
// 完全裸露这个条件如何破坏?
// 类型参数包裹数组
    
// 满足:
type test<T> = T extends string | number ? 1 : 2;
type a = test<1 | 2>
    
// 不满足:
type test<T> = [ T ] extends string | number ? 1 : 2;
type a = test<1 | 2>
    
// 再包裹泛型也可以破坏这个条件
type test<T> = test2<T> extends 1 | 2 ? 'y' : 'n'

2. 如果满足上述条件,即将这个联合类型拆开来,每个分支分别进行一次条件类型判断,再将最后的结果合并起来。最终会形成交集的效果:

// 示例:
type test<T> = T extends 1 | 2 | 3? 'y' : 'n';
type a = test<1 | 4>; // 'y' | 'n'
  • any 在作为判断参数时(不管是否裸露),只要此时判断条件类型不是any,都会产生分布式条件类型的效果
// 直接使用,返回联合类型
type Tmp1 = any extends string ? 1 : 2;  // 1 | 2

type Tmp2<T> = T extends string ? 1 : 2;
// 通过泛型参数传入,同样返回联合类型
type Tmp2Res = Tmp2<any>; // 1 | 2

// 如果判断条件是 any,那么仍然会进行判断
type Special1 = any extends any ? 1 : 2; // 1
type Special2<T> = T extends any ? 1 : 2;
type Special2Res = Special2<any>; // 1
  • never仅在通过泛型参数传入作为判断参数裸露时,会直接返回never
// 直接使用,仍然会进行判断
type Tmp3 = never extends string ? 1 : 2; // 1

type Tmp4<T> = T extends string ? 1 : 2;
// 通过泛型参数传入,会跳过判断
type Tmp4Res = Tmp4<never>; // never

// 如果判断条件是 never,还是仅在作为泛型参数时才跳过判断
type Special3 = never extends never ? 1 : 2; // 1
type Special4<T> = T extends never ? 1 : 2;
type Special4Res = Special4<never>; // never
  • 判断never类型
type isNever<T> = [T] extends [never] ? true : false
  • 判断any类型,需要利用any和其他类型交叉还会得到any的特性
type isAny<T> = 0 extends 1 & T ? true : false
  • 判断unknown类型
type isUnknown<T> = unknown extends T ? isAny<T> extends true ? false : true : false

七:类型层级顺序

  1. any unknown
  2. Object {}
  3. 装箱类型Number String Boolean
  4. 基础类型string number boolean undefined null symbol
  5. 对应的字面量类型
  6. never