理解 ts 类型间的各种关系,就看这棵树

本文对 typescript 及其安装做了简单介绍后,将可能用到的所有类型都做了梳理和测试,然后通过集合的角度再度梳理典型类型后,将所有类型绘制成了一棵类型集合继承树。除了本文最后提出的极少数情况外,所有类型的继承关系和数据赋值规则都遵从这棵类型集合继承树。

typescript 介绍

typescript 顾名思义,重点就在 type 上,它能给 javascript 加上类型系统,实现类型检查和类型提示,增强代码的可读性和可维护性,提高开发效率,以下简称为 ts

  • ts 是静态类型,编译时就能检查出类型错误,同时由于其是静态类型, 所以不能根据运行时数据。例如网络请求数据动态判断类型。
  • ts 是弱类型,即使类型检查报错,也会生成 js 文件。

ts 安装

  • 全局安装 ts: npm install -g typescript
  • 安装完成以后,会有全局命令行指令tsc,用于编译 ts 文件,生成 js 文件。
  • 编译 ts 文件:tsc xxx.ts,会生成 xxx.js 文件
  • 编译 ts 文件并监听:tsc xxx.ts -w,会生成 xxx.js 文件,并且监听 ts 文件的变化,一旦变化,就会重新编译。
  • 编译 ts 文件并指定输出目录:tsc xxx.ts -w -outDir ./dist,会生成 xxx.js 文件,并且监听 ts 文件的变化,一旦变化,就会重新编译,同时输出到 dist 目录下。

ts 代码示例 和 原始类型 primitive type

ts 定义变量和 js 一样,但是需要指定类型,其具体语法就是在变量后面加上冒号和类型,原始类型数据具体如下:

ts 复制代码
let isDone: boolean = false; // 布尔类型
let age: number = 10; // 数字类型
let name: string = "jack"; // 字符串类型
let sym1: symbol = Symbol("key1"); // symbol类型
let bigint2: bigint = 12345678901234567890n; // bigint类型
let undefined1:undefined = undefined // undefined类型
let null:null = null // null类型

关于 null 和 undefined

null 类型对应的值只能是 js 的原始数据 null(空引用或者说空对象)。但是在不打开strictNullChecks配置的情况下,null 和 undefined 两个类型被默认是其他类型的子类型。当然,bottom 类型never除外,事实上 bottom type 才是所有类型的子类型。

ts 复制代码
// strictNullChecks: false
let n: null = null;
let un: undefined = undefined;
let a: string = n; // ok
let b: number = un; // ok

由于这两个类型被默认是其他类型的子类型,所以有个奇怪的现象,null 和 undefined 类型的数据,可以相互赋值。这在代码理解上总给人一种错乱感。

ts 复制代码
// strictNullChecks: false
let n: null = null;
let un: undefined = undefined;
n = un; // ok
un = n; // ok

顶部类型 top type

首先明确两点,

  • ts 是遵循里氏替换原则的,在 ts 中理解成子类实例可以赋值给父类实例。不过在极少数情况下,ts是会违反里氏替换规则的。
  • js 中万事万物皆对象(可能undefined要除外)。即使true其实也是一个对象,它是Boolean构造函数的一个实例。

ts 的 top type 有两个:anyunknown,这个 top 主要指,他们处于类型集合继承树的顶端,是所有类型的父类型。根据里氏替换原则子类型的数据可以赋值给父类型的数据,继而得出 定义成top type的数据可以被任意类型的数据赋值

any

any 表示任意类型,可以赋值任意类型,如果将数据定义为了 any 类型,那么就会失去 ts 的类型检查。

  • 赋值方面:any 类型的数据,可以分配任何类型的值

    ts 复制代码
    let any1: any;
    any1 = 123;
    any1 = "";
  • 使用方面:any 类型的数据,可以当做任何类型的数据来使用

    ts 复制代码
    let any1: any = 1;
    any1.push(1); // 当数组使用,ok

unknown

unknown 表示未知类型,TypeScript 3.0 引入的,它是 any 类型对应的安全类型,所谓的安全,主要体现在数据使用的时候。unknown类型数据使用时,必须收敛成具体类型

  • 赋值方面 与 any 相同 unknown 类型的数据,可以分配任何类型的值

    ts 复制代码
    let unknown1: unknown;
    unknown1 = 123;
    unknown1 = "";
  • 使用方面 与 any 不同 unknown 类型的数据,几乎不可以当做任何一种类型来使用。

    ts 复制代码
    let unknown1: unknown;
    unknown1.push(); // 当做数组,报错❌
    let num: number = unknown1; // 当做数字,报错❌
  • unknown 类型数据使用前,必须使用类型断言astypeof判断,instanceof判断等方式收敛为具体类型。

    ts 复制代码
    let unk2: unknown;
    (unk2 as number).toFixed(2);
    
    let unk1: unknown;
    if (typeof unk1 === "number") unk1.toFixed(2);
    if (unk1 instanceof Number) unk1.toFixed(2);
  • unknown 类型联合任何类型,除 any 外结果都是 unknown 类型

    ts 复制代码
    type UnionType1 = unknown | string; // unknown
    type UnionType2 = unknown | any; // any
  • unknown 类型交叉任何类型,结果都是交叉的其他类型

    ts 复制代码
    type IntersectionType1 = unknown & string; // string
    type IntersectionType2 = unknown & any; // any

unknown类型几乎可以全面替换any类型。

底部类型 bottom type --- never

本章节需要有一定 ts 基础以后再看,可以先跳过。

ts 的 bottom type 只有一个 never。和 top 类型的含义一样, bottom 指其处于类型集合继承树的底部,是所有类型的子类型。根据里氏替换原则,子类的值可以赋值给父类型的数据,继而得出 never类型的数据可以赋值给任意类型的数据,因为没有子类型,never类型的数据只能被never类型数据赋值

never 类型表示的含义是永远不可能拿到的类型,也代表了空联合或者或空集合。

  • 赋值方面,never 类型的数据只能分配 never 类型的值。

    ts 复制代码
    function throwError(message: string): never {
      throw new Error(message);
    }
    let never1: never = throwError("Something went wrong");
    let num: number = never1; // ok,因为never是所有类型的子类型
    never1 = num; // 报错❌,never1是never类型的数据,只能被赋值成never类型。
  • 使用方面,作为所有类型的子类型,never 类型的数据可以赋值给任意其他类型。

    ts 复制代码
    function throwError(message: string): never {
      throw new Error(message);
    }
    let never1: never = throwError("Something went wrong");
    let num: number = 123;
    num = never1; // ok,never类型数据可以当做任一类型使用
    num.toFixed(2);
  • never 类型联合任何类型,结果都是联合的其他类型,这个结果和 unknown 相反

    ts 复制代码
    type UnionType1 = never | string; // string
  • never 类型交叉任何类型,结果都是 never,这个结果和 unknown 相同

    ts 复制代码
    type IntersectionType1 = never & string; // never
    type IntersectionType2 = never & any; // never
  • 验证 never 是比较难的

    ts 复制代码
    type IsNever<T> = T extends never ? true : false;
    type Res = IsNever<never>; // never

    上述例子的主要问题其实在于 extends 关键字,其前面是联合类型的裸泛型时(注意,这里其实有四个触发条件, 1、在 extends 关键字前面;2、联合类型;3、裸;4、泛型 ),会隐式触发分布式(将联合类型的每个子类型都应用到判断语句中得到一个子结果,最后再联合这些子结果) 。 而 never 被看做是空的联合类型, 所以其会触发分布式,但是由于其联合内没有任何子类型,导致后续判断逻辑无法进行。所以 extends never ? true : false碰到 never 类型的泛型 T 时,相当于不执行,直接返回前者的 T,即 never。根据上述条件,解除隐式触发分布式即可。

    ts 复制代码
    // 这里不再是裸泛型,同时元组也不是联合类型了。
    type IsNever<T> = [T] extends [never] ? true : false; 
    type Res = IsNever<never>; // true
  • never 常见使用场景之函数永不返回,当一个函数抛出异常、进入无限循环或根本不返回任何东西时,可以将其返回类型标记为 never。这样的函数被称为 never-returning functions

    ts 复制代码
    function throwError(message: string): never {
      throw new Error(message);
    }
    
    function infiniteLoop(): never {
      while (true) {
        // do something indefinitely
      }
    }
  • never 常见使用场景之永不可能发生的情况:当某个代码分支永远不会被执行时,可以将其类型标记为 never。

    • eg1:函数逻辑分叉

      ts 复制代码
      function processValue(value: number | string): void {
        if (typeof value === "number") {
          // handle number case
        } else if (typeof value === "string") {
          // handle string case
        } else {
          const exhaustivenessCheck: never = value;
          throw new Error(`Unhandled value: ${exhaustivenessCheck}`);
        }
      }
      // ps:这里把never类型的数据赋值给了void,这个是没问题的,因为never类型数据可以赋值给任何数据。
    • eg2:类型运算使用,配合条件判断隐式触发分布式never 在联合类型中的特殊性(联合类型中的 never 将被省略) ,收敛(narrowing)一些联合类型

      ts 复制代码
      type Exclude<Type, ToExcludedType> = Type extends ToExcludedType
        ? never
        : Type; // 排除Type中的ToExcludedType
      type Bc = Exclude<"a" | "b" | "c", "a">; // =>
      // ("a" extends "a" ? never : "a")|("b" extends "a" ? never : "b")|("c" extends "a" ? never : "c") =>
      // never | "b" | "c" =>
      // "b" | "c"
    • eg3:映射类型(从原对象类型产生新的对象类型),把对象相应的 key 转化成 never,从而达到过滤的效果。下例是从含有a,b,c三个键的对象类型中,过滤提取出a,b属性,组成一个新的对象类型

      ts 复制代码
      type ExtractMap<MapType extends {}, Keys> = {
        [key in keyof MapType as key extends Keys ? key : never]: MapType[key];
      };
      type ABCMap = { a: string; b: number; c: boolean };
      type ABMap = ExtractMap<ABCMap, "a" | "b">;
      // => { a: string; b: number; never: boolean }
      // => { a: string; b: number; }

特殊类型

上面的anyunknownnever应该也属于特殊类型,不过他们有更具体的分类,所以就单独提取出去了。

void

void 在 js 和 ts 都是关键字,不过与上面的 null 不一样,void 在 js 和 ts 上的含义和用法不太一样。

在 js 中,void 是一个运算符,它对紧跟其后的表达式求值。不管是什么表达式,void 总是返回真正的 undefined。这也是为什么很多早期代码中用void 0代表undefined的原因

ts 复制代码
const un = void 0; // undefined
function handle1() {
  return void 0;
}

用作自执行函数

ts 复制代码
void function fn() {
  // doSomething
}()

在不使用分号的代码风格中,使用 void 替代分号

ts 复制代码
let a = {}
void [9].push(1)

在 ts 中,void 主要应用在函数中,任何函数没有明确返回语句时,都会被推导为返回 void。同时它也是 undefined 的父类。

ts 复制代码
function fn() {} // => fn():void
type IsUndefinedExtendedFromVoid = undefined extends void ? true : false; // true
let un: undefined = undefined;
let void1: void = (() => {})();
void1 = un; // ok,根据里氏替换原则,子类型的数据(un)可以赋值给父类型的数据(void1)

上面例子中,基本体现不出来void类型和undefined类型的区别,其两者主要区别是用作函数返回值时,void作为返回类型,函数实现时可以返回任意类型,注意,这里是一个违反里氏替换规则的情况

ts 复制代码
let fn1: () => void = () => true; // ok,作为函数返回值时,void可以被true替换
let fn2: () => undefined = () => true; // 报错❌,不能将类型"boolean"分配给类型"undefined"

这个特性在内置函数中应该更好理解

ts 复制代码
//forEach类型定义: forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;
//push类型定义: push(...items: T[]): number;
const src = [1, 2, 3];
const dst = [0];

src.forEach((el) => dst.push(el)); // ok,这里foreach定义的参数函数返回值是void类型,但是参数函数返回值其实是push函数的返回值 number类型。void作为函数返回类型时,可以被其他类型替换。

不过这里有一个特例,那就是在函数字面量(function)定义的函数中显示要求返回 void 时,紧随的函数体的返回值 就只能是 void 类型了。

ts 复制代码
function fnWithFnBody1(): void {
  return true; //报错❌,不能将类型"boolean"分配给类型"void"
}

const fnWithFnBody2 = function (): void {
  return true; // 报错❌,不能将类型"boolean"分配给类型"void"
};

const fnWithFnBody3 = function (): void {
  return undefined; // ok,返回子类型是不会报错的
};

但是函数被赋值或者使用箭头函数时,void 可被其他类型覆盖的特性仍然不受影响。

ts 复制代码
let arrowFn: () => void = () => {
  // ok,箭头函数仍然可以使用string覆盖void
  return "";
};
const fnReturnString = () => "123"; // () => string

let implicitFn = function () {}; // () => void
implicitFn = fnReturnString; // ok,隐式推断返回值为void的函数可以使用string覆盖void

let explicitFn = function (): void {}; // () => void, 显式声明返回类型为void
explicitFn = fnReturnString; // ok,显式声明返回类型为void的函数仍然可以使用string覆盖void

void 也可以用来指定 this 类型,从而避免某些函数对 this 的使用。

ts 复制代码
function clickFn(this: void) {
  this.o = 0; // Property 'o' does not exist on type 'void'.
}
document.addEventListener("click", clickFn);

Object

  • Object 包含除 anyunknownvoidundefinednull 外的所有类型,也可以认为成包含了所有的非空类型。
ts 复制代码
let o: Object;
o = { prop: 0 }; //object
o = []; // array
o = 42; // number
o = "string"; // sting
o = false; // boolean
o = Symbol("key1"); // symbol
o = 12345678901234567890n; // bigint
o = Math.random; // function

同时它还有一些内建的方法

ts 复制代码
interface Object {
  /** The initial value of Object.prototype.constructor is the standard built-in Object constructor. */
  constructor: Function;

  /** Returns a string representation of an object. */
  toString(): string;

  /** Returns a date converted to a string using the current locale. */
  toLocaleString(): string;

  /** Returns the primitive value of the specified object. */
  valueOf(): Object;

  /**
   * Determines whether an object has a property with the specified name.
   * @param v A property name.
   */
  hasOwnProperty(v: PropertyKey): boolean;

  /**
   * Determines whether an object exists in another object's prototype chain.
   * @param v Another object whose prototype chain is to be checked.
   */
  isPrototypeOf(v: Object): boolean;

  /**
   * Determines whether a specified property is enumerable.
   * @param v A property name.
   */
  propertyIsEnumerable(v: PropertyKey): boolean;
}

let o: Object = {};
o.toString(); // built-in method => string

通过 new Object()创建的变量,将被认为是 Object类型

ts 复制代码
let obj = new Object(); // Object

重写内建方法,且与原始内建方法定义冲突时,会报错。

ts 复制代码
let obj: Object = {
  toString() {
    // 报错❌,不能将类型"number"分配给类型"string"
    return 123;
  },
};

{}

{}Object包含的范围是一致的,两者只有两点区别

  • {}类型不能通过new关键字创建

  • 重写内建方法,且与原始内建方法定义冲突时,不会报错。

    ts 复制代码
    let obj: {} = {
      toString() {
        // OK
        return 123;
      },
    };

{}一般被用来过滤nullundefined

ts 复制代码
/**
 * Exclude null and undefined from T
 */
type NonNullable<T> = T & {};

object

object{}的基础(不包含nullundefined)上,排除了剩余的原始类型(string, number, bigint, boolean, symbol),一般使用时,把它理解成 空对象({}) 的类型就可以了。当然对于一些特殊的对象,数组和函数,它也是包含的。

ts 复制代码
let obj: object = {
  toString() {
    // OK
    return 123;
  },
};
obj = 1; //报错❌, 不能将类型"number"分配给类型"object"
obj.valueOf(); // OK
obj = () => {}; // OK
obj = [1, 2, 3]; // OK

Function

可以认为是所有函数类型的父类型,其本身可以看作参数和返回值都是 any 的函数类型

ts 复制代码
type FnExtendedFormFunction = (() => void) extends Function ? true : false; // true
let fn: Function; // (...arg: any) => any;

对象类型

上面的Object{}object其实也属于对象类型。讲对象类型之前,请先确保你已经知道类型定义的两种方法。我们一般使用type或者interface关键字来定义对象类型。

ts 复制代码
interface Person {
  name: string;
  age: number;
}
ts 复制代码
type Person = {
  name: string;
  age: number;
};

当然,直接写字面量也行

ts 复制代码
let obj: { a: string; b: number };

对象表示树类型,直接递归即可:

ts 复制代码
type Tree = { value: string; left: Tree; right: Tree };

类型修饰符 readonly

一般来说,对象中定义的键值对,其值是可以修改的,不过总有一些场景,我们不希望值被修改(例如将一个对象当做参数传入函数中时),这个时候可以使用 readonly 修饰符来修饰这个键值对,将其变成只读属性:

ts 复制代码
interface SomeType {
  readonly staticProp: string;
  normalProp: string;
}
let a: SomeType = { staticProp: "staticProp", normalProp: "normalProp" };
a.staticProp = ""; // 报错❌, 无法为"staticProp"赋值,因为它是只读属性
a.normalProp = ""; // ok

和上面例子相反,有些场景下,我们需要修改一些只读属性的值,例如拷贝了一个对象,要修改这个新对象中的数据时。那就需要使用-readonly在类型中移除只读限制。

ts 复制代码
// 这个例子用到了泛型和循环对象类型的一些知识点,可以后续再看。
type CreateMutable<Type> = {
  // 通过'-'移除Type键值对中的readonly
  -readonly [Property in keyof Type]: Type[Property];
};

type LockedAccount = {
  readonly id: string;
  readonly name: string;
};

type UnlockedAccount = CreateMutable<LockedAccount>;

类型修饰符 ?

?修饰符代表的含义是,对象中该键值对是可选(非必须)地。该修饰符在做一些配置项时特别实用。

ts 复制代码
interface Config {
  id: string;
  name: string;
  description?: string; // 可选属性
  options?: {
    enabled: boolean;
    timeout?: number; // 嵌套的可选属性
  };
}

const config1: Config = {
  id: "123",
  name: "My Config",
  description: "This is a config",
  options: {
    enabled: true,
    timeout: 5000,
  },
};

const config2: Config = {
  id: "456",
  name: "Another Config",
};

ts 内置类型别名Partial就是通过?实现的

ts 复制代码
/**
 * Make all properties in T optional
 */
type Partial<T> = {
  [P in keyof T]?: T[P];
};

同样地,也可以用-移除可选修饰符?,内置类型别名Required实现如下

ts 复制代码
/**
 * Make all properties in T required
 */
type Required<T> = {
  [P in keyof T]-?: T[P];
};

索引签名 Index Signatures

很多情况下我们并不能一次性描述所有的 key,例如伪字符串数组,只知道它的成员都是 string 类型的,有一个叫做 length 的成员,它的值是 number 类型

ts 复制代码
interface StringArray {
  length: number;
  [index: number]: string; // 这里的index可以改成任意名称,只是一个变量代指所有的number类型的key
}

固定项符合索引签名规则时,其类型也必须符合索引类型值的限制。

ts 复制代码
interface Obj {
  length: number; // 报错❌,'length'符合索引签名([key: string]),其值的类型必须符合索引类型的限制, 即 boolean类型
  [key: string]: boolean; 
}

而索引签名的更高级应用 映射类型,我们将在后面详细介绍。

数组类型

在 ts 中,数组内的成员被要求只能是同一种类型。例如字符串数组,数字数组,某种对象数组等,其定义形式有两种。

  • 使用数组泛型Array<T>定义

    ts 复制代码
    interface Person {
      name: string;
      age: number;
    }
    type StringArray = Array<string>;
    type NumberArray = Array<number>;
    type PersonArray = Array<Person>;
  • 使用[]字面量定义

    ts 复制代码
    interface Person {
      name: sting;
      age: number;
    }
    type StringArray = string[];
    type NumberArray = number[];
    type PersonArray = Person[];

只读数组 ReadonlyArray

除了数组类型外,ts 还提供了只读数组类型。只读数组类型的数据,没有内建修改数组的方法,例如 push,pop,shift,unshift 等,数组成员本身不允许增删和覆盖。

  • 使用只读数组泛型ReadonlyArray定义

    ts 复制代码
    interface Person {
      name: string;
      age: number;
    }
    type StringArray = ReadonlyArray<string>;
    type NumberArray = ReadonlyArray<number>;
    type PersonArray = ReadonlyArray<Person>;
    let p: PersonArray = [{ name: "jack", age: 18 }];
    p[0] = { name: "jack", age: 18 }; // 报错❌, 类型"PersonArray"中的索引签名仅允许读取。数组成员本身不允许增删和覆盖。
    p[0].name = "tom"; // ok,这个其实和readonlyArray没有关系了,属于改变数组成员内部的值,只和数组成员定义的类型有关
  • 使用字面量readonly Type[]定义

    ts 复制代码
    interface Person {
      name: string;
      age: number;
    }
    type StringArray = readonly string[];
    type NumberArray = readonly number[];
    type PersonArray = readonly Person[];

从继承上,可以认为,非只读类型继承了只读类型,并扩展了一些内建方法,如push,修改成员项接口等

ts 复制代码
type ReadOnlyT = readonly string[];
type NormalT = string[];
type IsNormalTExtendedFromReadOnlyT = NormalT extends ReadOnlyT ? true : false; // true

根据里氏替换原则,子类数据可以赋值给父类数据。即非只读数据可以赋值给只读数据,反之则不能。

ts 复制代码
let readonlyData: readonly string[] = [];
let normalData: string[] = [];

readonlyData = normalData; // ok
normalData = readonlyData; // 报错❌,类型 "readonly string[]" 为 "readonly",不能分配给可变类型 "string[]"

元组 Tuple

元组可以认为是长度固定,且内部成员类型明确的数组,内部成员间类型可以不同。

ts 复制代码
type StringNumberPair = [string, number];

只读元组

和只读数组类似,ts 还提供了只读元组类型。语法也和只读数组字面量语法类似。

ts 复制代码
type LockedStringNumberPair = readonly [string, number];

只读元组和非只读元组之间也符合上述只读类型是非只读类型父类型的原则

ts 复制代码
type StringNumberPair = [string, number];
type LockedStringNumberPair = readonly [string, number];
let stringNumberPair: StringNumberPair = ["1", 2];
let lockedStringNumberPair: LockedStringNumberPair = ["1", 2];

lockedStringNumberPair = stringNumberPair; // ok, 非只读类型可以赋值给只读类型
stringNumberPair = lockedStringNumberPair; // 报错❌,类型 "LockedStringNumberPair" 为 "readonly",不能分配给可变类型 "StringNumberPair"

函数类型

这里只对函数类型定义做一些简单讲解,其他像可选参数,剩余参数,函数重载,逆变协变等将在后续文章中展开。

ts 复制代码
// 接口定义
interface IFn {
  (a: string, b: number): number; // 冒号后面为返回值
}

// 类型别名定义
type IFn2 = (a: string, b: number) => number; // 纯类型时箭头后面是返回值。

使用function关键字定义,有类型推断的存在,写成下文fn1的形式,明确必要类型就可以了

ts 复制代码
// 明确参数类型
function fn1(a: string, b: number) {
  return a.length + b;
}

// 明确参数和返回值类型。杂糅在函数体中时,返回值在小括号后面,用 :Type 表示
function fn2(a: string, b: number): number {
  return a.length + b;
}

使用箭头函数定义,有类型推断的存在,写成下文fn1的形式,明确必要类型就可以了

ts 复制代码
// 明确参数类型
const fn1 = (a: string, b: number) => a.length + b;

// 明确参数和返回值类型。 杂糅在函数体中时,返回值在小括号后面,用 :Type 表示
const fn2 = (a: string, b: number): number => a.length + b;

// 明确变量 fn3 的类型
const fn3: (a: string, b: number) => number = (a, b) => a.length + b;

// 明确 变量fn4、参数和返回值 的类型
const fn4: (a: string, b: number) => number = (a: string, b: number): number =>
  a.length + b;

class

本小节主要讲类的类型,并不会对类其他方面做过多介绍。

  • 当定义一个类的时候,实例的类型是类本身

    ts 复制代码
    class Person {
      public name: string;
      public age: number;
      constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
      }
    }
    const person = new Person("tom", 18);
    person.name; // string
    person.age; // number
  • 而要获取类本身的类型需得通过typeof关键字

    ts 复制代码
    class Person {
      public name: string;
      public age: number;
      static staticKey: number = 0;
      constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
      }
    }
    const person = new Person("tom", 18);
    person.name; // string
    person.age; // number
    person.staticKey; // 报错❌, 属性"staticKey"在类型"Person"上不存在
    
    type PersonType = typeof Person;
    type PersonKeys = keyof PersonType; // "prototype" | "staticKey"
  • 一个不需要参数的类构造函数的类型为 new () => T,其实需要参数在相应位置填入参数即可。

    ts 复制代码
    class Person {
      public name: string = "";
      public age: number = 0;
    }
    
    function createInstance<T>(ctor: new () => T): T {
      return new ctor();
    }
    
    const person = createInstance(Person); // Person

其他类对象类型

其他类对象类型,例如map,set,promise等,和Array<T>泛型类似,都是通过内建相应的Map<K, V>,Set<T>,Promise<T>泛型实现的。

其实对象类型的重头戏在类型映射,不过在了解完联合类型以后,对象类型的赋值也特别重要。

字面量类型 Literal Types

我们并不需要总是为数据绑定类型,ts 系统在很多情况下可以根据类型定义推断出数据的类型,它甚至专门为推断定义了一个关键字infer

ts 复制代码
let str = "abc"; // string
let bool = true; // boolean
let num = 123; // number
let obj = { name: "jack", age: 18 }; // { name: string; age: number }
type ReturnType<T extends Function> = T extends (...args: any) => infer R // infer后续会详细讲解
  ? R
  : never;
type FnReturnType = ReturnType<() => string>; // string

在 js 中,使用const定义变量,是不允许再被重新赋值的,我们一般称呼其为常量。而在 ts 中,通过const定义的 原始类型数据 会被推断成相应字面量的类型。

ts 复制代码
let str = "abc"; // string
const str2 = "abc"; // 'abc'
const bool = true; // true
const num = 123; // 123

// 非原始数据不会被推断为字面量类型
const obj = { name: "jack", age: 18 }; // { name: string; age: number }

字面量推断

除了推断原始类型为字面量类型外,ts 其实也可以推断出字面量对象和字面量元组,但是要借助as const这两个关键字。并且推断出来的对象属性将直接变成只读属性。元组则直接变成只读元组。

ts 复制代码
const obj = { name: "jack", age: 18 } as const; // { readonly name: "jack"; readonly age: 18 }
const tuple1 = ["jack", 18] as const; // readonly ["jack", 18];

类型定义的两种方法

专门用来定义类型的关键字有两个typeinterface。类型定义,建议首字母大写

类型别名 type

开发过程中我们可能会复用一些类型,例如两个函数的参数都要用到相同的参数类型。这个时候,我们会希望将这个类型定义成一个类似 js 中变量的形式,方便在不同的地方复用这个类型。而在 ts 中我们称这个类似 js 变量的 ts 变量为类型别名(Type Aliases),它由 type 关键字定义。

ts 复制代码
type ParamType = { name: string; age: number };
function fn1(arg: ParamType) {}
function fn2(arg: ParamType) {}

接口 interface

对象类型的定义一般由两种方式,一种是上文展示的使用type关键字定义,还有一种就是使用interface关键字定义。

ts 复制代码
interface ParamType {
  name: string;
  age: number;
}

type 和 interface 的相同点

  • 都可以创建对象类型
  • 都可以拓展对象类型

type 和 interface 的不同点

  • interface 不能定义原始类型数据,但type可以

  • interface 可以给已存在的类型添加属性,但type不行

    ts 复制代码
    interface Person {
      name: string;
    }
    
    interface Person {
      age: number;
    }
    let person: Person;
    person.name; // string
    person.age; // number
  • 两者拓展对象类型的方式不同

    ts 复制代码
    // interface 通过extends关键字
    interface Animal {
      name: string;
    }
    
    interface Bear extends Animal {
      honey: boolean;
    }
    
    let bear: Bear;
    bear.name; // string
    bear.honey; // boolean
    ts 复制代码
    // type 通过交叉类型操作符 &
    type Animal = {
      name: string;
    };
    
    type Bear = Animal & {
      honey: boolean;
    };
    let bear: Bear;
    bear.name; // string
    bear.honey; // boolean
  • 在报错提示中,如果数据不符合接口相关定义,会提示到具体不符合哪个接口的定义。但如果不符合类型别名的定义,则错误提示体现不出来具体违背了哪个类型别名的定义(这个应该是因编辑器而异的,vscode 上两者都能定位到。)

    ts 复制代码
    interface Mammal {
      name: string;
    }
    
    function echoMammal(m: Mammal) {
      console.log(m.name);
    }
    
    // 报错信息会提示违背了接口Mammal.name的类型定义
    echoMammal({ name: 12343 });
    ts 复制代码
    type TMammal = { name: string };
    function echoAnimal(m: TMammal) {
      console.log(m.name);
    }
    // 某些编辑器上,报错信息只会提示违背了某个属性name的定义,但是不会提现TMammal
    echoAnimal({ name: 12345 });

    在两者使用上,typescript handbook 的建议是尽量使用interface

类型树

行文至此,我们基本上讲解测试了 ts 所有的类型。本节则主要是从集合的角度,分析类型,然后梳理绘制出一颗类型继承树。在此之前,要先了解下联合类型和交叉类型

联合类型 Union Types

  • 联合类型表示所有联合项的并集。是所有联合项的父类型,通过|操作符表示,可以读作

    ts 复制代码
    // 原始类型的联合
    type StringOrNumber = string | number;
    ts 复制代码
    // 对象类型的联合
    interface Obj1 {
      name: string;
      age: number;
    }
    interface Obj2 {
      name: string;
      male: boolean;
    }
    type Obj1OrObj2 = Obj1 | Obj2;
  • 定义为联合类型的数据,在类型收敛(精确)之前,只能使用联合项共有的方法或者属性

    ts 复制代码
    // 原始类型的联合
    type StringOrNumber = string | number;
    type Keys = keyof StringOrNumber; // "toString" | "valueOf"
    let obj: StringOrNumber; // 则obj只能用字符串和数字共有的属性  "toString" 或者 "valueOf"
    ts 复制代码
    // 对象类型的联合
    interface Obj1 {
      name: string;
      age: number;
    }
    interface Obj2 {
      name: string;
      male: boolean;
    }
    type Obj1OrObj2 = Obj1 | Obj2;
    type Keys = keyof Obj1OrObj2; // "name"
    let obj: Obj1OrObj2; // obj除了可以用 name外,其实还可以使用泛型Object上定义的接口,如toString
  • 联合 类型的数据 可以被 联合项类型的数据 赋值,类型收敛之后,可以使用相关收敛类型的 key。

    ts 复制代码
    // 原始类型的联合
    type StringOrNumber = string | number;
    type keys = keyof StringOrNumber; // "toString" | "valueOf"
    let obj: StringOrNumber;
    obj = 1; // 此时obj的类型已经被收敛为 联合项类型 number
    type numberKeys = keyof typeof obj; // "toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"
    ts 复制代码
    // 对象类型的联合
    interface Obj1 {
      name: string;
      age: number;
    }
    interface Obj2 {
      name: string;
      male: boolean;
    }
    type Obj1OrObj2 = Obj1 | Obj2;
    type keys = keyof Obj1OrObj2; // "name"
    let obj: Obj1OrObj2 = { name: "sr", age: 10 };
    
    // 这里obj被的类型被收敛为 联合项类型 Obj1
    type Obj1Keys = keyof typeof obj; // keyof Obj1  => name | age

交叉类型 Intersection Types

交叉类型表示所有交叉项的交集。是所有交叉项的子类型,通过&操作符表示,可以读作

  • 原始类型的交叉类型,一般来说都是 never,没什么意义

    ts 复制代码
    // StringAndNumber 既是string的子类型,又是number的子类型,只有never。
    type StringAndNumber = string & number; // never
  • 对象类型的交叉类型,理解成包含所有交叉项的 key 的对象类型就可以了

    ts 复制代码
    interface Obj1 {
      name: string;
      age: number;
    }
    interface Obj2 {
      name: string;
      male: boolean;
    }
    type Obj1AndObj2 = Obj1 & Obj2;
    type keys = keyof Obj1AndObj2; //  "name" | "age" | "male"

需要特别注意的对象类型赋值

对象类型赋值,在对象联合类型下,某些情况不会进行类型收敛。字面量赋值,则只能是具体类型的数据才会赋值成功。

  • 非字面量赋值的情况下,对象符合里氏替换原则,可以被子类型数据赋值,并且会将父类型数据收敛成子类型数据。在联合类型中,则是收敛成某一联合项,如果无法收敛成某个具体的联合项,那类型将不进行收敛。

    ts 复制代码
    interface Obj1 {
      name: string;
      age: number;
    }
    interface Obj2 {
      name: string;
      male: boolean;
    }
    
    type Obj1OrObj2 = Obj1 | Obj2;
    type keys = keyof Obj1OrObj2; // "name"
    let obj: Obj1OrObj2;
    let superObj = { name: "sr" };
    let obj1 = { name: "sr", age: 10 };
    let obj2 = { name: "sr", male: true };
    let subObj = { name: "sr", age: 10, male: true };
    let narrowSubObj = { name: "sr", age: 10, male: true, other: "other" };
    
    // ok,符合里氏替换原则
    let normalObj: Obj1 = narrowSubObj;
    
    // 报错❌,因为superObj的类型 不属于 任何一个 联合项
    obj = superObj;
    
    // ok,obj会被收敛成Obj1类型,可以使用obj.age
    obj = obj1;
    
    // ok,obj会被收敛成Obj1类型,可以使用obj.male
    obj = obj2;
    
    // ok, 但此时由于subObj既是Obj1的子类型,又是Obj2的子类型,所以obj无法收敛成具体联合项,只能是Obj1OrObj2类型
    obj = subObj;
    
    // ok, 但此时由于subObj既是Obj1的子类型,又是Obj2的子类型,所以obj无法收敛成具体联合项,只能是Obj1OrObj2类型,值得注意的是,只要是子类型即可,哪怕更精确了一个名为other的key,也没问题
    obj = narrowSubObj;
  • 字面量类型赋值情况下,不再符合里氏替换原则,赋值的数据必须和类型要求一致。在对象联合类型中,(1)字面量和某一个联合项类型一致,类型会收敛成该联合项。(2)字面量和所有联合项的交叉类型一致,因为不能收敛成具体联合项,所以不进行收敛。其他情况的赋值则不被允许。

    ts 复制代码
    interface Obj1 {
      name: string;
      age: number;
    }
    interface Obj2 {
      name: string;
      male: boolean;
    }
    
    type Obj1OrObj2 = Obj1 | Obj2;
    type keys = keyof Obj1OrObj2; // "name"
    let obj: Obj1OrObj2;
    
    // 报错❌,这里赋值的数据必须和类型要求一致
    let normalObj: Obj1 = { name: "sr", age: 10, male: true };
    
    // ok,obj会被收敛成Obj1类型,可以使用obj.age
    obj = { name: "sr", age: 10 };
    
    // ok,obj会被收敛成Obj1类型,可以使用obj.male
    obj = { name: "sr", male: true };
    
    // ok,但此时由于subObj既是Obj1的子类型,又是Obj2的子类型,所以obj无法收敛成具体联合项,只能是Obj1OrObj2类型
    obj = { name: "sr", age: 10, male: true };
    
    // 报错❌,因为多了other类型,无法收敛成具体联合项或者他们的共有精确子类型
    obj = { name: "sr", age: 10, male: true, other: "other" };

ts 中的父子类型

在 js 代码class Dog extends Animal中,我们认为Dog类继承了Animal类,AnimalDog的父类或者超类(supertype),DogAnimal的子类或者亚类(subtype)。子类中有父类所有的方法和属性,同时子类还可以扩展自己的方法和属性。这种需要特殊关键字才构成的继承关系,我们称之为名义继承(nominal typing)

ts 作为 js 的超类,其类型继承上采用的却是另一种方案:结构继承(structural typing) ,结构型继承的特点是只要一个类型Foo包含了另一个类型Bar的所有成员,那这个类型Foo就是Bar的子类型,即使Foo类型还有其他成员也无所谓。

虽然 ts 是结构继承,但它仍然可以使用extends支持名义继承,只是在 ts 中extends关键字除了有继承的作用外,还承担着产生条件类型的责任,其语法为 type ConditionalType = SomeType extends OtherType ? TrueType : FalseType;,如果符合前面的继承条件SomeType extends OtherType,则新类型ConditionalTypeTrueType,否则是FalseType。甚至可以说 ts 通过判断两个类型的继承关系实现了条件语句。

条件类型主要是配合泛型(Generics),类型推断关键字infer使用,我们将在后续章节详解。

ts 复制代码
type IsUndefinedExtendedFromVoid = undefined extends void ? true : false; // true

通过集合角度描述 ts 的类型

现在有一个类型{ name: string},我们从语义的角度描述它就是限制name为string类型的所有对象。转换成集合图示它是这样的:

再来一个类型{ age: number},我们从语义的角度描述它就是限制age为number类型的所有对象。转换成集合图示它是这样的:

现在有了第三个对象类型{ name: string; age: number},它含义是 限制name为string类型 且 限制age为number的所有对象 ,通过语义转代码可以转成{ name: string}&{ age: number}。而转换成集合,就是上述两个类型的交集,即图中的两个颜色块重合的位置。

{ name: string}|{ age: number}被转化成集合图时,其实就是上图中全部有色区域。

如果把每一个键值对都描述成一种限制的话,从上述三张图其实能得出一个结论,集合内限制越多,则成员越少

接下来看下原始类型如何用集合来表示,就以number为例。

原始数据类型依然符合集合内限制越多,则成员越少 的结论。

number包含了所有字面量数字类型,所以其成员就包含所有数字类型,字面量类型1限制了子类型只有数字类型1,所以其成员就只有一个(实际上还有never,以及父元素上的所有共用的方法之类的,但是那样二维很难画出来,这里只是比对集合的大小,当做只有一个成员也没问题)。
1number的交集1 & number就是图中深色1所代表的区域,即1本身。而1number的并集集1 | number就是父类型number本身。所以这个时候可以得到第二个结论集合的交集就是交叉类型即子类型,集合的并集就是联合类型即父类型

那么特殊类型object如何表示呢,我们知道它包含了除原始类型外的所有类型(实际还要排除any,unknown和void),集合图示如下:

Object{}表示的集合,就是object集合再扩展出其余非空原始类型:

在此基础上,加上nullundefinedvoid就是anyunknown代表的集合了,注意undefinedvoid是有父子关系的:

至此,除never外所有类型都通过集合表示完了。而never本身代表的含义是空联合,或者说空集合,即其内部一个子元素也没有,一个子元素都没有的集合也就意味着,其是任何集合的子集合。

如果要画二维图的话,所有类型相交的那个点就是never,这基本画不出来。而转化成代码表示的话,就是所有类型的交集即:SomeType1 & SomeType2 & SomeType3 ...

而通过层级树的结构,则恰好可以把never也囊括进去,同时也会更清晰的表明什么是top type,什么是 bottom type

类型集合继承树

我们以所有类型的父类型anyunknown作为顶点,开始绘制一棵类型集合树,每层之间是继承关系,上一层是下一层的父类型,那么never就处于所有类型的最底层,其形如下

树顶的就是top type:anyunknown

树底的就是bottom type never

里氏替换原则在类型集合继承树上的表示就是可以从下向上替换。

顺着箭头向下的方向表示类型越来越具体,限制越来越多,同时也是类型结构不断继承的过程,这里描述成集成也对。

通过这棵树再看类型的赋值和分析类型间的继承关系就会简单很多。

  • unknown

    ts 复制代码
      let stringVariable: string = 'string'
      let unknownVariable: unknown 
    
      unknownVariable = stringVariable // ok,从下向上赋值
      stringVariable = unknownVariable // 报错❌,从上向下赋值
  • never

    ts 复制代码
      let stringVariable: string = 'string'
      let anyVariable: any
      let neverVariable: never
    
      neverVariable = stringVariable // 报错❌,从上向下赋值
      neverVariable = anyVariable // 报错❌,从上向下赋值
      anyVariable = neverVariable // ok,从下向上赋值
      stringVariable = neverVariable // ok,从下向上赋值
  • void

    typescript 复制代码
      let undefinedVariable: undefined 
      let voidVariable: void
      let unknownVariable: unknown
    
      voidVariable = undefinedVariable // ok,从下向上赋值
      undefinedVariable = voidVariable  // 报错❌,从上向下赋值
      voidVariable = unknownVariable  // 报错❌,从上向下赋值

ts违背里氏替换原则情况总结

这里主要说的是类型赋值时,不符甚至逆转里氏替换原则的情况。

逆转情况(函数逆变)

当一个函数类型被赋值时,其参数类型是逆里氏替换的,即参数值的类型得是原函数定义的父类型。函数逆变在后续函数章节会详细解释。

ts 复制代码
type SupertypeFn = (obj: { a: string; b: number }) => void;
type SubtypeFn = (obj: { a: string; b: number; c: number }) => void;
let superFn: SupertypeFn = (superObj) => {};
let subFn: SubtypeFn = (subObj) => {};
subFn = superFn; // ok,函数被赋值时,参数是逆变的。
superFn = subFn; // 报错

不可替换情况(这两种其实都是对象类型直接使用字面量赋值)

对象赋值的情况,详细内容请查看需要特别注意的对象类型赋值

  • 函数参数传入字面量类型的情况
ts 复制代码
function fn(obj: { name: string }) {}
fn({ name: "foo", key: 1 }); // 报错
  • 直接给变量赋值字面量类型的值
ts 复制代码
type UserWithEmail = { name: string; email: string };
type UserWithoutEmail = { name: string };
let userB: UserWithoutEmail = { name: "foo", email: "foo@gmail.com" }; // 报错

void的特殊情况

除了 使用function关键字且显示返回void的情况,函数体可以返回任何类型

ts 复制代码
let arrowFn: () => void = () => {
// ok,箭头函数仍然可以使用string覆盖void
  return "";
};
const fnReturnString = () => "123"; // () => string

let implicitFn = function () {}; // () => void
implicitFn = fnReturnString; // ok,隐式推断返回值为void的函数可以使用string覆盖void

let explicitFn = function (): void {}; // () => void, 显式声明返回类型为void
explicitFn = fnReturnString; // ok,显式声明返回类型为void的函数仍然可以使用string覆盖void

参考资料

相关推荐
有梦想的刺儿13 分钟前
webWorker基本用法
前端·javascript·vue.js
cy玩具34 分钟前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
清灵xmf1 小时前
TypeScript 类型进阶指南
javascript·typescript·泛型·t·infer
qq_390161771 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test2 小时前
js下载excel示例demo
前端·javascript·excel
Yaml42 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事2 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶2 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo2 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v2 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript