TypeScript学习笔记,吐血整理

1,122 阅读18分钟

写在前头,该文是看半途而废的ts官网学习、掘金上的文章以及项目实践总结而来。

1. TypeScript类型

基本类型-常用类型

一些常用的就不赘述了,包括以下

  • Boolean类型
  • Number类型
  • String类型
  • Array类型
  • Any类型
  • Void类型
  • Null和Undefined类型
  • objct类型和Object类型和{}类型

对象类型又有 objct 和 Object 和 {} ,对象类型非常常用,那这三种有什么区别呢?

  • objct类型是指非原始类型

包括七大原始类型,这里经过百度搜索,范围锁定为1个月内,还看到很多写五个原始类型的... 橙色波浪线就是不符合TS检验的语句

  • Object 类型是所有 Object 类的实例的类型

Object 类型有两个接口

  1. Object 接口定义了 Object.prototype 原型对象上的属性
  2. ObjectConstructor 接口定义了 Object 类的属性

那么问题出现了,原型对象上的属性和类上的属性有什么区别呢?

这里就不讲了,有兴趣复习的可以面向搜索引擎查找一下

  • {} 类型

按我的理解,这个类型指的就是 Object 类的实例。

你想表示里面有什么属性就往上面写,由于{} 类型是 Object 类的实例,所以在使用 Object.prototype 原型对象上的属性的时候,TypeScript也不会提示错误,比如:

    // people有两个属性,分别是sex和name
    let people: { sex: string; name: string };
    // 属性正确,ts不会报错
    people = { sex: 'man', name: '吴亦凡' };
    // 属性错误,ts会报错
    people = { a: '1' };
    // 使用Obect类接口上的属性,ts不会报错
    people.toString();
    

基本类型-不常用类型

  • Symbol类型

原因很简单,因为 Symbol 我用得少

  • Unknown类型

我们知道 any 可以表示任何类型,但是这样会削弱ts规范类型,避免错误的作用。为了解决 any 带来的问题,TypeScript 3.0 引入了 unknown 类型来表示不知道是什么类型的意思。unknown 类型有以下特性

  1. unknown 类型可以使用任何类型来赋值(既然变量不知道是什么类型,我就可以为所欲为了)
    let value: unknown;
    value = 1;
    value = 'a';
    value = { a: 1 };
  1. 任何不是 any 或者 unkown 类型的值不可以使用 unkown 类型来赋值(我是一个有身份的变量,不可以这么随便)
    let value: unknown;
    value = 1;
    // ERROR: 不能将类型“unknown”分配给类型“number”。
    const count: number = value;

个人觉得使用场景在某个变量可能是存在多个类型的,unkown类型可以帮助我们标示出变量可能有多个类型,当我们要对特定变量处理的时候,可以使用类型守卫(具体在下文解释)让满足类型变量的条件通过。

    let value: unknown;
    value = 1;
    if (typeof value === 'number') {
      value += 1;
    } else if (typeof value === 'string') {
      value = parseInt(value, 10) + 1 
    }
  • Enum类型

枚举类型有各种各样枚举方式,数字/字符串/常量/异构,这些枚举方式我个人觉得不需要怎么记,使用场景就是有时候我们需要一些数字或者字符串常量,但是直接看常量并不知道它代表的意思,这时候我们可以使用枚举来对常量进行类型命名

    // 枚举顺序从0开始,这里常量0代表待发布
    enum Status {
      UNPUBLISHED,
      PUBLISHED,
    }
    // 相当于type Status = 0 | 1;但使用枚举可以定义每个值的含义
    // Status的属性可以表示从0到1的常量
    let status: Status = Status.PUBLISHED;
  • Tuple类型

这个类型叫做元组,跟数组有关的,数组类型定义的是一组具有相同类型的数组,比如number[]代表一组数字组成的数组。那元组跟数组有什么区别呢?

  1. 元组是确定数量的数组
  2. 元祖数组中的每个属性都有自己对应的类型

根据以上区别举个例子

let tupleType: [string, boolean];
tupleType = ["semlinker", true];
  • Never类型

Never类型代表永远不会存在的值的类型,比如没有返回值的函数。在实际应用中,我发现这个类型也可以用来排查错误。当我们对数据进行一系列操作时,TypeScript提示某个变量是never类型,但这个类型不应该是never类型时,可以知道在代码中我们可能进行了错误的操作。

Never类型还有一种我没有用过的用法,用于检查联合类型。下面查看一下代码。

type Foo = string | number;

function controlFlowAnalysisWithNever(foo: Foo) {
  if (typeof foo === "string") {
    // 这里 foo 被收窄为 string 类型
  } else if (typeof foo === "number") {
    // 这里 foo 被收窄为 number 类型
  } else {
    // foo 在这里是 never
    const check: never = foo;
  }
}

这里Foo是一个联合类型(联合类型就是可以是几种类型中的一种),以上代码编译后不会出错,但是一旦这个Foo被他人修改

type Foo = string | number | boolean;

那么条件判断里最后的never类型赋值就会被TypeScript报红提示错误。通过这种形式有助于减少bug的数量。

interface-接口

前文说过关于对象类型除了Objectobject还有直接使用{},如果要表示某类对象,一直使用{}定义非常繁琐,interface提供来描述对象的形状,称之为接口。当然,interface除了用于描述对象的形状,还能用于描述类的抽象行为。

描述对象的形状

对象形状接口写法如下:

interface Person {
  name: string;
  age: number;
}

该接口描述了一类对象,他们拥有name和age的属性,并且描述了属性对应的类型。这样定义的对象在使用时需要完全匹配其形状,但有的时候,对象中的某些属性是可选的,那么可以使用可选属性。

interface Person {
  name: string;
  age: number;
  hair?: boolean;
}

有些人可能没有头发, 即使拥有了可选属性,但如果我想让这个对象相对于可以添加任意其他属性,可选属性不可能遍历所有我们添加的属性,这时候可以使用任意属性的写法。需要注意的是,使用任意属性之后,所有固定属性和可选属性的类型都必须是任意属性的类型的子集,比如以下代码,任意属性的类型为anystringnumber都是它的子集,编译不会报错,如果把任意属性的类型改为string,则会出现报错。

interface Person {
  name: string;
  age: number;
  [propName: string]: any;
}

还有一种特殊属性为只读属性,在定义对象时必须赋值,且往后不能对只读属性进行修改,代码如下

interface Person {
  readonly id: number;
  name: string;
  age: number;
  [propName: string]: any;
}

id为只读属性,在对对象第一次赋值时,必须要对id赋值,且往后不能对id进行修改

描述类的抽象行为

一般来说,类只能继承自另一个类,但是有时候不同的类具有相同的特性,把这些特性抽象起来,写成接口。不同的类可以使用implements关键字实现多个抽象接口。举个例子,防盗门和车子属于不同的类,但是都有共同的特性-具有报警器功能。可以把报警器抽象成接口,让防盗门和车子实现报警器,具体代码如下:

// 报警器接口-定义类的抽象行为
interface Alarm {
    alert(): void;
}

// 大类-表示一类门
class Door {
}

// 子类-表示防盗门,继承于门,实现了报警器功能
class SecurityDoor extends Door implements Alarm {
    alert = () => {
      console.log('SecurityDoor alert');
    };
}

// 车子类,与防盗门类不一样,但是可以实现同样的报警器功能
class Car implements Alarm {
    alert = () => {
      console.log('Car alert');
    };
}

接口的继承和合并

接口和接口之间是可以继承的,不管是描述类的抽象行为还是描述对象的形状,在继承之后,不仅需要满足原来的接口还需要满足继承的接口条件,比如:

// 描述类的抽象行为
interface Alarm {
    alert(): void;
}

interface LightableAlarm extends Alarm {
    lightOn(): void;
    lightOff(): void;
}

class lightableAlarm implements Alarm {
  alert = () => {};

  lightOn = () => {};

  lightOff = () => {};
}

// 描述对象的形状
interface BasePerson {
  id: number;
}

interface Person extends BasePerson {
  age: number;
}

const pserson: Person = { id: 1, age: 1 };

// 甚至混用(不知道有没有实际意义,聪明的你教教我)
interface BasePerson {
  id: number;
}

interface Person extends BasePerson {
  eat(): void;
  walk(): void;
}

const pserson: Person = { id: 1, eat: () => {}, walk: () => {} };

class PersonClasss implements Person {
  id = 1;

  eat = () => {};

  walk = () => {};
}

当接口多次定义时,会对属性进行合并

interface Point { x: number; }
interface Point { y: number; }

const point: Point = { x: 1, y: 2 };

联合类型、交叉类型与类型别名

在写前端的代码时,我们常常使用||表示或逻辑、&&表示与逻辑,而在TypeScript中,也有相应的字符代表类型的或、与逻辑。 type Age = number | undefind表示或逻辑,称之为联合类型,表示Age类型可以取数值类型或者undefind,这符合我们的直观感觉,那type Age = number & undefind又代表什么意思呢,当我们使用Age类型的时候,在编辑器上悬浮在该类型上,可以发现它实际上是nerver类型。因为不存在一种类型,它既是数值类型又是undefind类型,它是一种不存在值的类型。`

那么交叉类型能用在哪里呢?答案是对象,在以下例子中,我们定义了Head对象和Body对象,并对类型People定义为HeadBody的交叉类型,现在People便会拥有他们两个所有的属性类型。如果存在同名属性,将会把属性类型进行联合,如果各自的类型不一样,将有可能导致never类型的出现。

代码中type People,称为类型别名,可以理解为重命名类型的名字,当使用联合类型或交叉类型书写过长而且多次使用该类型,使用类型别名,可以减少代码量,且更具有语义。

interface Head {
  hair: number;
  eys: number;
}

interface Body {
  head: number;
  foot: number;
}

type People =  Head & Body

函数

参数类型与返回类型

函数可以定义参数的类型以及返回的类型,参数类型在函数参数中定义,返回类型在function()后定义。

javascript中有一些参数是可选的,typeScript也可以表达可选参数,只需在定义类型的:前加上?变成?: string便能表示一个可选的string类型的参数。使用可选参数时,需要注意,可选参数需要在所有必须参数的最后,不然会导致编译失败。

let IdGenerator: (chars: string, nums: number) => string;

function createUserId(name: string, id: number): string {
  return name + id;
}

// 可选参数
function createUserId2(name: string, id: number, age?: number): string {
  return name + id;
}

IdGenerator = createUserId;

函数重载

名字真帅

我们书写JavaScript函数时会出现一种情况,不同类型的函数,返回类型也有可能不一样的,使用联合类型确实可以解决typeScript语义及编译问题。但是有时候我们不同的参数类型,返回类型时固定的,比如参数为number类型,返回number类型,参数为string类型,返回string类型,这样联合类型就力不从心了,因为联合类型无法做到根据已知的参数类型来适应对应的返回类型,这时候就是函数重载出场的时候了。

以下函数,参数类型为联合类型,返回类型可以是number,也可以是string,接下来我们使用函数重载的写法,对add函数定义了4种情况,根据参数类型不同,返回类型也不同,最后定义整个函数体。


type Combinable = string | number;

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
  if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString();
  }
  return a + b;
}

add(1, 2); // function add(a: number, b: number): number (+3 overloads)
add("1", 2); // function add(a: string, b: number): string (+3 overloads)

函数重载除了可以使用在普通的函数中,也可以使用在类的成员函数中,写法与普通函数基本一致。

2. 断言

  • 类型断言
    • 尖括号<number>someValue
    • someValue as number
  • 非空断言
  • 确定赋值断言

类型断言

用于给告诉TypeScript某个值你非常确定是你断言的类型,而不是他推测出来的类型。

举个简单的例子,更新按钮只有在id存在的时候出现,点击时调用更新方法,这个时候就可以使用类型断言把id指定为number类型。


const Element = () => {
  let id: number | undefined;

  const onUpdate = (updateId: number) => {
    return updateId;
  };

  const onAdd = () => {
    return null;
  };
  return (
    <div>
      {id 
        ? <a onClick={() => onUpdate(id as number)}>编辑</a> 
        : <a onClick={onAdd}> 新增</a>}
    </div>
  );
};

非空断言

它可以告诉TypeScript某个值不是null、undefined,其形式为在变量后添加一个!,我们可以对上面的更新方法进行重写,在调用的时候就不用对id进行类型断言可以达到一样的效果

  ...
  const onUpdate = (updateId: number | undefined) => {
    return updateId!;
  };
  ...
  <a onClick={() => onUpdate(id)}>编辑</a>
  ...

确定赋值断言

它用于告诉TypeScript某个值已经被赋值,举个例子

// Error
let x: number;
initialize();
// Variable 'x' is used before being assigned.(2454)
console.log(2 * x);

function initialize() {
  x = 10;
}

// OK
let y!: number;
initialize();
console.log(2 * x);

function initialize() {
  y = 10;
}

所有的断言只能使我们通过TypeScript的类型检测,并不会帮我们修正可能错误的代码,比如我们使用了确定赋值断言,但是实际上我们并没有对其进行赋值,编译后的代码依然会是undefind

3. 类型守卫

好帅的名字

印象中我并没有类型守卫这个名词,于是特地在官网上搜索了一下,但是并没有搜索到任何内容,可能是我搜索的关键字不对,类型守卫包括in/typeof/instanceof(为了简写,下称三守卫)以及类型谓词。我的理解是,TypeScript对通过守卫的变量类型进行了收窄的处理,在使用了类型守卫后,TypeScript可以从原有类型中推测出符合守卫条件的类型。

string守卫: 这条路只能由类型为string的通过!number守卫:俺只给number类型放行。Cat类守卫:喵喵喵。喵?

三守卫

为了方便理解,举个例子

const typeGuard = (param: { type: string; value: string } | { type: string; name: string }) => {
  if ('value' in param) {
    // 悬浮在param上,ts确切给出param属于第一种类型
    return param.value;
  }
  if ('name' in param) {
    // 悬浮在param上,ts确切给出param属于第二种类型
    return param.name;
  }
  return undefined;
};

typeGuard({ type: 'a', value: 'typea' });

这里举了三守卫中的in,剩余两种的用法也基本一致,其本质都是TypeScript根据语法分析进行了类型的收窄,那么在进行类型检查时,就会符合JavaScript原有的类型判断效果。

类型谓词

使用三守卫可以覆盖大多数的类型守卫场景,但也有无法覆盖的场景,举个例子

interface Cat {
  type: string;
  belong: string;
  name: string;
  say: 'mewo';
}

interface Dog {
  type: string;
  belong: string;
  name: string;
}

type Animal = Cat | Dog;

如上代码,我们无法通过三守卫在Animal类型中区分出Cat类型和Dog类型,这时候可以使用类型谓词来自定义类型守卫

function isTinaCat(animal: Animal): animal is Cat {
  return animal.belong === 'tina' && animal.type === 'cat';
}

function isTinaDog(animal: Animal): animal is Dog {
  return animal.belong === 'tina' && animal.type === 'dog';
}

上面两个函数有点像平时我们用于判断符合某种条件的通用方法,只是这个函数的返回值类型有点特别,它不是任何类型,它是一个使用了谓语的语句。具体形式为:函数参数 is 类型名,称之为类型谓语。

类型谓语用于标示该函数是一个类型保护的函数,返回true表示符合某种类型,反之不符合。

下面使用是类型谓语的具体使用例子

function tinaNameTheAnimal(animal: Animal) {
  if (isTinaCat(animal)) {
    // animal为Cat类型,这里悬浮在animal上,可以看到是有say属性的对象,证明是Cat类型
    animal.name = '啊猫';
  }
  if (isTinaDog(animal)) {
    animal.name = '啊狗';
  }
}

const littleCat = { type: 'cat', belong: 'tina', name: '', say: 'mewo' }

// 把littleCat命名为啊猫
tinaNameTheAnimal(littleCat);

4. 范型

对象的类型可以通过接口进行复用,函数可以通过重载达到通过不同的输入类型配对不同的输出类型,使用任意属性可以让对象添加属性,只要符合规定的类型。即使是如此灵活的规则,仍有没有覆盖的地方。

在我的项目中,曾有一种情况:每个查询数据的请求会返回不同类型的nodes数据以及total查询总数。在这种情况下,不可能使用函数重载遍历所有的查询语句,因为查询函数应用的地方非常多。又比如我们常常使用useState后数组解构赋值后,可以获取到的状态和改变状态函数,React不可能猜中我们需要使用的类型。anyScript是不可能anyScript的,那怎么解决这一类问题呢?

在这种情况下,如果类型能够被传递和使用,像变量一样,我们就可以自己传入类型来进行类型约束,范型就是专门帮助我们解决这类问题的。

以上可以理解为类型形参,使用<>代表包裹在里面的是类型形参。他代表使用函数者传递的某种类型。以上图片可以理解为接收一个类型,例如调用identity<string>('a')传入了string类型,那么T所到之处都替换成了string类型,即参数valuestring类型,函数返回string类型。

除了T代表类型Type之外,通常作为类型变量的名称,但实际上可以用任何有效名代表类型变量,常见的类型变量命名有以下:

  • K(key):代表类型中的键类型
  • V(value):代表类型中的值类型
  • E(element):代表元素类型

而类型变量的数目并不是只有一个,与函数参数一样,可以传如多个类型变量,比如<T,U>便是接收两个类型变量,当接收到实际的类型时,分别替换其出现的位置。

当然除了显式得写好类型变量,编译器可以从函数参数中推测你需要传入得类型变量分别是什么类型,例如identity(68, "Semlinker");,编辑器可以从68推测出T的类型为number,而从"Semlinker"推测出Ustring类型 。

范型除了应用于函数,还可以使用于接口和类。范型可以传递自定义类型,而无需为函数进行重载或者不得已使用any一言蔽之,使得类型更为清晰。

工具类型

  1. typeof可以帮助我们获取变量或者常量的类型。当类型深藏于第三方库中或可以从已赋值的字面量推测出来时,可以使用type a = typeof xx来获取xx的类型,并命名为a使用。
  2. keyof可以获取某种类型的所有键,有点像Object.keys,但是keyof可以获取原型链上的键
  3. in用于枚举联合类型
type Keys = "a" | "b" | "c"

type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any, c: any }

  1. infer用于承载传入的类型并进行类型的推测
type ParamType<T> = T extends (param: infer P) => any ? P : T;

整句表示为:如果T能赋值给(param: infer P) => any,则结果是(param: infer P) => any类型中的参数P,否则返回为T

interface User {
  name: string;
  age: number;
}

type Func = (user: User) => void

type Param = ParamType<Func>;   // Param = User
type AA = ParamType<string>;    // string

类型Func能赋值给(param: infer P) => any,P承载了传入的User,根据条件判断,返回PUser类型,所以Parm类型为User。string不能赋值给(param: infer P) => any,所以直接返回其本身Tstring

  1. extends用于对范型的类型进行约束
interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

传入的类型必须符合Lengthwise接口。

  1. Partial<T>用于把接口中的属性都变为可选属性

5. TypeScript 4.0 新特性

构造函数的类属性推断

noImplicitAny配置属性被启用之后,TypeScript 4.0 可以确认类中的属性类型:

class Person {
  fullName; // (property) Person.fullName: string,在 3.9.2 版本下,会报错:Member 'fullName' implicitly has an 'any' type.(7008)
  firstName; // (property) Person.firstName: string
  lastName; // (property) Person.lastName: string

  constructor(fullName: string) {
    this.fullName = fullName;
    this.firstName = fullName.split(" ")[0];
    this.lastName =   fullName.split(" ")[1];
  }  
}


标记的元组元素

在使用函数时,可以使用...args来表示剩余函数参数,当描述剩余函数参数的类型可以使用元组进行描述(...args: [string, number]),但是这样子我们就丧失了剩余函数参数参数名,为了使剩余函数参数拥有参数名提示,可以在元组中设置参数的名称。

function addPerson(...args: [name: string, age: number]): void {
  console.log(`Person info: name: ${args[0]}, age: ${args[1]}`);
}

这样,当我们使用剩余函数参数使,将会有参数名的提示。


参考文献

  1. 在 TS 中如何实现类型保护?类型谓词了解一下
  2. 一份不可多得的 TS 学习指南(1.8W字)
  3. TypeScript官网
  4. 巧用 TypeScript(五)---- infer