Skip to content

TypeScript 重学

TypeScript 编译

TypeScript 官方没有做运行环境,只提供编译器。编译时,会将类型声明和类型相关的代码全部删除,只留下能运行的 JavaScript 代码,并且不会改变 JavaScript 的运行结果。因此,TypeScript 的类型检查只是编译时的类型检查,而不是运行时的类型检查。一旦代码编译为 JavaScript,运行时就不再检查类型了。

TypeScript 类型

知识回顾

JS 的类型【8:7+1】

基本类型:numberstringbooleannullundefinedsymbolbigInt

对象类型:Object

TS 类型:

基本类型:NumberStringBooleanNullundefinedSymbolBigInt

对象类型:FunctionObjectArrayEnum(枚举)

特殊类型:VoidAnyUnkownNever

联合类型:A | B

交叉类型:A & B

值类型

type:取别名

Typeof:会将值/函数转化为类型

Any 类型

any 类型除了关闭类型检查,还有一个很大的问题,就是它会"污染"其他变量。它可以赋值给其他任何类型的变量(因为没有类型检查),导致其他变量出错。

ts
let x: any = "hello";
let y: number;

y = x; // 不报错;因为 x 是 any 类型,ts 会关闭 x 的类型检测

y * 123; // 不报错
y.toFixed(); // 不报错

unknow 类型

那么 unknow 解决了这个问题:变量 vunknown 类型,赋值给 anyunknown 以外类型的变量都会报错,这就避免了污染问题。

在集合论上,unknown 也可以视为所有其他类型(除了 any)的全集,所以它和 any 一样,也属于 TypeScript 的顶层类型。

Array 类型

TypeScript 数组有一个根本特征:所有成员的类型必须相同,但是成员数量是不确定的,可以是无限数量的成员,也可以是零成员。

由于数组的成员数量可以动态变化,所以 TypeScript 不会对数组边界进行检查,越界访问数组并不会报错

只读数组

只读数组的含义就是:只允许进行查看,不允许进行任何的操作

ts
const arr: readonly number[] = [1, 2, 3]

只读数组的写法:

  1. 关键字写法:readonly number[]
  2. 泛型写法:ReadonlyArray<number> / ReadOnly<number[]>
  3. Const 断言:const 是一个常量,那么他设置的数组,就是只读数组

Const 断言

const 是一个常量,他设置的值,都是不能再变化的。

JS 的基本类型

  1. Symbol 类型会被断言为 unique symbol 类型
  2. 除了 Symbol 以外,const 断言后的类型,就会变为值类型

对于,对象类型而言,就会变为,只读类型

元组

特点:长度固定,并且每个索引位置的类型是确定的

ts
const s: [string, string, boolean] = ["a", "b", true];

场景:在 React 里,useStateuseReduceruseContext 等 Hook 返回的就是元组。

当然元组的长度也可以不固定:

  1. 加上可选符号(?):可选运算符必须放在最后面
  2. 加上扩展运算符(...):扩展运算符用在元组的任意位置都可以,但是他后面的必须要数组/元组
ts
let a: [number, number?] = [1]; // 可选运算符
let b: [string, ...number[]] = ["hello", 1, 2, 3] // 扩展运算符

函数类型

  1. 普通函数写法
  2. 箭头函数写法
ts
// 写法一
const hello = function (txt: string) {
  console.log("hello " + txt);
};

// 写法二
const hello: (txt: string) => void = function (txt) {
  console.log("hello " + txt);
};

如果一个变量要套用另一个函数类型,有一个小技巧,就是使用 typeof 运算符

ts
function add(x: number, y: number) {
  return x + y;
}

const myAdd: typeof add = function (x, y) {
  return x + y;
};

参数默认值

设置了默认值的参数,那么该参数是可选的。如果不传入该参数,它就会等于默认值。设有默认值的参数,如果传入 undefined,也会触发默认值。

ts
function createPoint(x: number = 0, y: number = 0): [number, number] {
  return [x, y];
}

createPoint(); // [0, 0]

function f(x = 456) {
  return x;
}

f2(undefined); // 456:设置了默认值的参数,即使传递的参数是 undefined,那么也是使用默认值

函数重载:

一个函数可以接受不同类型或不同个数的参数,并且根据参数的不同,会有不同的函数行为这种根据参数类型不同,执行不同逻辑的行为,称为函数重载(function overload)。

ts
function reverse(str: string): string;
function reverse(arr: any[]): any[];
function reverse(stringOrArray: string | any[]): string | any[] {
  if (typeof stringOrArray === "string")
    return stringOrArray.split("").reverse().join("");
  else return stringOrArray.slice().reverse();
}

函数重载的每个类型声明之间,以及类型声明与函数实现的类型之间,不能有冲突。

ts
// 报错
function fn(x: boolean): void;
function fn(x: string): void; // 这一个和下面那个冲突了,他并不知道应该执行哪一个
function fn(x: number | string) {
  console.log(x);
}

那么其实我们,函数重载也并不是必要的:**联合类型替代函数重载。**因为我们可以通过定义参数的类型【联合类型】,然后对参数类型进行判断,从而来执行不同的逻辑。

ts
// 写法一
function len(s: string): number;
function len(arr: any[]): number;
function len(x: any): number {
  return x.length;
}

// 写法二
function len(x: any[] | string): number {
  return x.length;
}

对象类型【object】

对象类型比较宽泛,一般情况下,我们不要将类型直接定义为 Object 类型

属性名的索引类型【少用】

产生场景:有些时候,无法事前知道对象会有多少属性,比如外部 API 返回的对象,没法去定义这个对象的类型

索引类型里面,最常见的就是属性名的字符串索引。

ts
type MyObj = {
  [property: string]: string;
};

const obj: MyObj = {
  foo: "a",
  bar: "b",
  baz: "c",
};

但是索引类型要少用,因为这样他会增大我们的类型范围!

接口【interface】

interface 可以表示对象的各种语法,它的成员有 5 种形式。

  • 对象属性
ts
interface A {
    name: string
}
  • 对象的属性索引
ts
interface A {
    [prop: string]: string;
}
  • 对象方法
ts
// 写法一
interface A {
  f(x: boolean): string;
}

// 写法二[常写!!!]
interface B {
  f: (x: boolean) => string;
}

// 写法三
interface C {
  f: { (x: boolean): string };
}
  • 函数
ts
interface Add {
    (a: number, b: number): number
}
  • 构造函数

特性

继承【extends】

  1. 继承可以继承一个也可以继承多个;其实我们可以将它理解为,联合类型。
  2. 如果子接口与父接口存在同名属性,那么子接口的属性会覆盖父接口的属性。注意,子接口与父接口的同名属性必须是类型兼容的,不能有冲突,否则会报错。
  3. 接口可以继承接口,接口也可以继承类型,还有继承 Class
ts
interface Style {
  color: string;
}

interface Shape {
  name: string;
}

interface Circle extends Style, Shape {
  color: number; // 报错:类型不兼容
  radius: number;
}

接口合并

多个同名接口会合并成一个接口。但是他们的同名属性必须可以兼容!

Type 和 interface 的区别

相同点:都可以用来描述对象

不同点:

  1. Type 能够表示非对象;但是 interface 只能表示对象

    ts
    type A = number; // type 有起别名的作用
  2. interface 可以继承其他类型,type 不支持继承。

    interface 中,继承使用的是 extends;但是 type 可以通过 & 来实现增加属性的效果

    ts
    type Animal = {
      name: string;
    };
    
    type Bear = Animal & {
      honey: boolean;
    };
    
    interace Animal = {
      name: string;
    };
    
    interface Bear extends Animal {
    honey: boolean;
    }
  3. 类型合并问题

    Interface 的类型是可以合并的,但是同名 type 就会报错。

    TypeScript 不允许使用 type 多次定义同一个类型。

  4. interface 不可以使用属性映射【keyof】,但是 type 可以

    ts
    interface Point {
      x: number;
      y: number;
    }
    
    // 正确
    type PointCopy1 = {
      [Key in keyof Point]: Point[Key];
    };
    
    // 报错
    interface PointCopy2 {
      [Key in keyof Point]: Point[Key];
    };
  5. interface 无法表达某些复杂类型(比如交叉类型和联合类型),但是 type 可以

    结论:一般情况下,优先考虑 interface;如果会涉及到比较复杂的类型运算,那么才会使用 type

Enum 枚举

Enum 结构的特别之处在于,它既是一种类型,也是一个值。绝大多数 TypeScript 语法都是类型语法,编译后会全部去除,但是 Enum 结构是一个值,编译后会变成 JavaScript 对象,留在代码中。

ts
// 编译前
enum Color {
  Red, // 0
  Green, // 1
  Blue, // 2
}

// 编译后
let Color = {
  Red: 0,
  Green: 1,
  Blue: 2,
};

多个同名的 Enum 结构会自动合并。但是在合并的时候,只允许其中一个的首成员省略初始值,否则报错,并且合并的时候,不允许出现同名成员。

ts
enum Foo {
  A,
}

enum Foo {
  B, // 报错
}