Ts基础知识和内置类型

408 阅读17分钟

基础

extends 关键词

extends有三种用途:继承、泛型约束、分配(条件类型)

1. 继承/扩展

一方面和 js 作用一致,用在 class 上。 另一方面,也可以继承类型,只有interface可用

 interface Person { name: string }
 
 interface Child extends Person { age: number }
 // interface Child = { name: string; age: number }

2. 泛型约束

在泛型中,有时需要约束类型参数,可以借助extends来完成,比如

// 希望传入的参数是个函数
type Type<T extends (props: any) => void>= { info: T };
const obj: Type<() => void> = {info: function(){}} 

// obj中的person属性必须包含name属性
interface Type<T extends { name: string }> {
  person: T;
}
const obj: Type<{ name: string; age: number }> = {
  person: { name: 'hxy', age: 25 },
};

3. 分配(条件类型)

extends还可以用来判断一个类型是否可以分配给另一个类型,先看例子

type A = { name: string };
type B = { name: string };
type C = Child extends Person ? string : number;
// type C = string

和三元运算一样,第三行的意思是,A类型 是否可以分配给 B类型?是的话 C 为string,否则 C 为number。这里的 A 和 B 类型一致,当然可以分配。 这里就可以明白上面第二条的泛型约束 T1 extends T2,意思就是 T1 需要可以分配给 T2。 需要强调的是,**A 可以分配给 B,不等于 A 是 B 的子集。**再看例子

type A = { name: string; age: number };
type B = { name: string };
type C = A extends B ? string : number; 
// type C = string

这里的 A 显然不是 B 的子集,但条件判断依然成立(依然可以分配)。 这样的错误判断是因为用集合论的思想来理解类型系统了,即如果一个集合 A 的所有元素在集合 B 中都存在,则 A 是 B 的子集。 在类型系统中,先说结论:**两个类型间,更具体的类型是子类型,更宽泛的类型是父类型,子类型可以分配(赋值)给父类型。**举两个例子来加深理解:

  1. 上面的例子,A 的属性更多,它更具体, B 的属性少,它就更宽泛,所以 A 是子类型,所以 A 可以分配给 B。和类来类比理解,比如class A extends B {},显然 A 是子类 B 是基类,A 当然可以分配给 B,用结论来解释的话,A 可能在 B 的基础上扩展属性变得更具体。
  2. type Type = 1 | 2 extends 1 ? 'yes' : 'no'。Type 是 no,因为1|2有两个选项更宽泛(父),1只有一个选项更具体(子)。

有了 extends 可以用作分配的基础知识,下面来详细看看条件类型

条件类型

从上文明白条件类型的基本用法,下面看一个搭配泛型使用的例子

interface IdLabel {
  id: number;
}
interface NameLabel {
  name: string;
}
 
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
  throw "unimplemented";
}

这里是函数重载的写法,作用是描述 createLabel 函数基于输入值的类型不同而返回不同的类型。可以看到这里只有两个类型,就需要重载三次,如果有一天要加一个类型,重载的次数会呈指数增加。 写js代码时我们通常会使用三元运算符减少一些if/switch语句,上面的例子同样可以用条件类型优化

type NameOrId<T extends number | string> = T extends number
  ? IdLabel
  : NameLabel;

此时可以通过这个条件类型简化函数重载

function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
  throw "unimplemented";
}
 
let a = createLabel("typescript");
// let a: NameLabel
 
let b = createLabel(2.8);
// let b: IdLabel
 
let c = createLabel(Math.random() ? "hello" : 42);
// let c: NameLabel | IdLabel

分发条件类型

type Example1 = 'x' extends 'x' ? string[] : number[];
// type Example1 = string[]
type Example2 = 'x' | 'y' extends 'x' ? string[] : number[];
//type Example2 = number[]

type ToArray<T> = T extends 'x' ? string[] : number[];
type Example3 = ToArray<'x' | 'y'>; // ?

直觉告诉我Example3number[],实际上是string[] | number[],因为若给 ToArray 传入一个联合类型,这个条件类型会被分发到联合类型的每个成员

ToArray<'x' | 'y'>;
// 相当于
ToArray<'x'> | ToArray<'y'>;
// 结果显然就是 string[] | number[]

通常这是我们期望的行为,如果要避免这种行为,可以用方括号包裹 extends 关键字的每一部分。

type ToArray<T> = [T] extends ['x'] ? string[] : number[];
type Example3 = ToArray<'x' | 'y'>;
// type Example3 = number[];

infer 关键词

条件类型的基本语法 T extends U ? X : Y; 如果占位符类型 U是一个可以被分解成几个部分的类型,比如数组类型、元组类型、函数类型、字符串字面量类型等。这时候可以通过 infer 来推断 U 类型中某个部分的类型。 infer使用限制:只能被用在条件类型中,只能在**true**分支使用。

// 1. 推断数组(或者元组)的类型
type ArrayItem<T> = T extends (infer U)[] ? U : never;
type Example1 = Type1<string[]>
// type Example1 = string
type Example2 = ArrayItem<[number, string]>
// type Example2 = string | number

// 2. 推断数组(或者元组)第一个元素的类型(最后一个写法类似)
type First<T extends unknown[]> = T extends [infer P, ...infer _] ? P : never
type Example3 = First<[3, 2, 1]>
// type Example3 = 3

// 3. 推断函数类型的参数 和 推断函数类型的返回值,内置了Parameters 和 ReturnType

readonly 关键词

可以在数组、元组、接口、对象类型上使用readonly。 类里的字段加一个 readonly 前缀修饰符,会阻止在构造函数之外的赋值。 as const也会把字段转为只读,一般用在数组和对象上。

type T1 = readonly [number, string] // 元祖
type T2 = readonly string[] // 数组
type T3 = {readonly name: string}
type T4 = readonly string // ❌ 不允许这样写

class Person {
  readonly name: string = "hxy";

  // 只允许在constructor里修改
  constructor(otherName?: string) {
    if (otherName !== undefined) {
      this.name = otherName;
    }
  }
}

const arr = [1,2] as const
// const arr: readonly [1, 2]
const arr = {name: 'hxy'} as const
// const arr: { readonly name: "hxy"; }

keyof 操作符

TS 中keyof操作符只能用于类型,不能用于值,Class 除外,因为在 TS 中 Class 本身也可作为类型。往往keyof需要搭配typeof使用。

计算属性名

因为计算属性名的类型只能是string、number、symbol或any,所以当不确定键的类型时,keyof通常会返回string|number|symbol

const key1 = 1;
const key2 = 'string';
const key3 = Symbol();

const Obj = {
  [key1]: 1,
  [key2]: 2,
  [key3]: 3,
};

type T = keyof typeof Obj;
// type T = 1 | "string" | typeof key3
// key3 只可能是 string、number、symbol或any,所以typeof key3等于string|number|symbol

所以如下代码会报错,因为没有约束T的类型

function useKey<T, K extends keyof T>(o: T, k: K) {
  var name: string = k;
  // error 不能将类型“string | number | symbol”分配给类型“string”.
}

// 当然是可以使用Extract解决
function useKey<T, K extends Extract<keyof T, string>>(o: T, k: K) {
  var name: string = k;
}

用于Class

keyof可以直接用在class上,但要注意,静态属性不会被keyof获取,除此之外所有实例属性都会被获取。

class Base {
  name: string = 'xxx';
  static age = 'ccc';
  fun = () => {};
  fun2() {}
}

type T = keyof Base;
const a: T = 'age';// error
const a: T = 'name'; // ok

typeof 操作符

TS 限制了可以使用 typeof 的表达式的种类,只有对标识符(比如变量名)或者他们的属性使用,下面的例子是典型的错误

const Type = typeof fun();

看起来像是获取函数返回值的类型,但是函数调用不是标识符或属性,显然不能使用(可以使用ReturnType实现) **typeof**部分使用示例:

// ----用于对象
const person = { name: "hxy", age: 24 }
type My = typeof person;
// type My = { name: string; age: number }

// ----用于函数,对函数使用是很常用的用法,比如使用 Parameters 和 ReturnType 内置类型就需要对函数用typeof
function fun<Type>(arg: Type): Type {
  return arg;
}
type Fun = typeof fun;
// type Fun = <Type>(arg: Type) => Type

// ----用于枚举
enum ActionType {
  ADD = 'add',
  DELETE = 'delete',
}
type T1 = typeof ActionType;
// type T = {
//   ADD: ActionType.ADD;
//   DELETE: ActionType.DELETE;
// };
const a: T1 = { ADD: ActionType.ADD, DELETE: ActionType.DELETE };
// 看起来毫无意义,一般可以搭配keyof获取枚举的键
type T2 = keyof typeof ActionType
// type T2 = "ADD" | "DELETE"

接口interface

命名对象类型的另一种方式,下面主要来看接口和类型别名的区别:

  1. 接口可以合并,类型别名不可以

接口重复定义时会合并属性,但是如果出现定义重复属性,重复属性的类型需要一致,否则会报错。 重复定义类型别名会报错,类型别名如果想要扩展,需要使用交叉类型的方式,交叉时出现重复属性不会报错,但是可能会成为unknown类型。

interface Person { name: string }

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

interface Person { name: number }
// error 后续属性声明必须属于同一类型。属性“name”的类型必须为“string”,但此处却为类型“number”。ts(2717)

type Animal = { name: string }
type Animal = { age: number }
// error 标识符“Animal”重复。ts(2300)

// 交叉类型扩展
type NewAnimal = Animal & { 
  age: number 
}

// name变为never类型
type NewAnimal = Animal & { 
  name: number 
}
  1. 接口可以使用extends,类型别名不可以

使用extends也是一种扩展属性方式,不能用在类型别名上,类型别名还是使用交叉类型来扩展。

  1. 接口只能定义对象类型,类型别名没有限制

索引访问类型

用于获取一个类型上的特定属性,要获取属性的对象和索引名都需要是一个类型

type Person = { age: number; name: string };
const key1 = 'age';
type key2 = 'age';
type AgeType1 = Person[key1]; // error key1不是类型
type AgeType2 = Person[key2]; // ok, type AgeType2 = number
// 我们经常会将联合类型等作为索引名
type keyType1 = Person[keyof Person]; // type keyType1 = string | number
type keyType2 = Person['age' | 'name']; // type keyType2 = string | number

用于数组

结合typeof可以通过索引number获取到数组字面量的类型

const MyArray = [{ name: 'hxy', age: 24 }];
// 从左往右运算,先typeof再索引访问
type My = typeof MyArray[number];
// type My = {
//   name: string;
//   age: number;
// }

可以这么理解,数组本身就是一个键为数字的对象,所以可以通过number索引获取数组字面量类型

// 数组是一个类似于下面的实现
interface Arr {
  [index: number]: any;
}
// 准确的实现是
interface Array<T> {
	at(index: number): T | undefined;
}

当然要注意,我们一般不能通过string索引获取对象的属性

type Person = { age: number; name: string };
type keyType = Person[string]; // 类型“Person”没有匹配的类型“string”的索引签名
// 当然如果这样定义就可以了
type Person = { [key: string]: number };
type keyType = Person[string]; // type keyType = number

映射类型

一般会和keyof搭配使用。先来看一个最基本的例子,将 Type 所有属性设置为布尔类型

type BecomeBoolean<T> = {
  [Property in keyof T]: boolean;
};

映射修饰符

在映射类型中,可能会用到readonly?修饰符。可以通过+``-来增加或去掉修饰符,没写相当于+

type BecomeReadable<T> = {
  -readonly [Property in keyof T]: T[Property];
};
type Type = {
  readonly id: string;
  readonly name: string;
};
type ReadableType = BecomeReadable<Type>
// type ReadableType = {
//     id: string;
//     name: string;
// }
 

利用as重新起键名

type Getters<Type> = {
    [Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};
 
interface Person {
    name: string;
    age: number;
    location: string;
}
 
type LazyPerson = Getters<Person>;

这个Getters的实现利用到了Capitalize,它用于将一个字符串模版字面量类型的首字母转为大写。 在as后面跟的其实可以是一个类型,上面的例子相当于跟的是字符串模版字面量类型。

type BecomeString<T> = {
  [Property in keyof T as string]: T[Property];
};

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

type NewType = BecomeString<Person>;
// type NewType = {
//     [x: string]: string | number;
// }

在联合类型中,never会被过滤掉,所以我们就可以利用never来过滤对象类型中的某个属性:

type RemoveKind<T> = {
    [Property in keyof T as Exclude<Property, "kind">]: T[Property]
};
 
interface Type {
    kind: number;
    name: string;
}
 
type NoKind = RemoveKind<Type>;
// type NoKind = {
//     name: string;
// }

Exclude<Property, "kind">,可以分配给'kind'的类型会返回never,所以在联合类型中会被过滤掉。 在映射类型中,我们当然不用局限于使用keyof,遍历的是联合类型就可以。也不用局限于string | number | symbol这种简单基本的联合类型,可以是任何类型的联合:

type EventConfig<Events extends { kind: string }> = {
    [E in Events as E["kind"]]: (event: E) => void;
}
 
type SquareEvent = { kind: "square", x: number, y: number };
type CircleEvent = { kind: "circle", radius: number };
 
type Config = EventConfig<SquareEvent | CircleEvent>
// type Config = {
//    square: (event: SquareEvent) => void;
//    circle: (event: CircleEvent) => void;
// }

泛型

Generics,泛型就是被用来描述两个值的类型之间的对应关系,我一般会用函数参数的概念来理解,泛型就是传入一个类型参数。

泛型约束

泛型可以被约束,上文extends关键词中有提到。

泛型函数

函数中有两个或以上的类型有关联,比如多个参数或者参数和返回值的类型有关联,考虑使用泛型函数。

function firstEle<Type>(arg: Type[]): Type {
  return arg[0];
}

// 当然也可以这么写
type T = <Type>(arg: Type[]) => Type;
const firstEle: T = arg => arg[0];

// 可以不传入类型参数,ts会根据参数推导出泛型的类型
firstEle([1, 2, 3]);
// function firstEle<number>(arg: number[]): number

// 当然可以自己传,只是没必要
firstEle<number>([1, 2, 3]);

泛型对象

需要注意的是,和泛型函数不同,泛型对象必须传入类型参数,无法通过属性的类型推导,不过可以有默认值。

interface Box<Type> {
  contents: Type;
}

const a: Box<number> = {contents: 1}

interface Type<T = string>{
	name: T // T默认为string
}

泛型类

泛型类不一定要传入类型参数,像泛型函数一样会推导。 一个类的类型有两部分:静态部分和实例部分。泛型类仅仅对实例部分生效,所以当使用类的时候,注意静态成员并不能使用类型参数

class Person<Type1, Type2> {
  name: Type1;
  age: Type2;
  getInfo: (x: Type1, y: Type2) => Type1;
  static field: Type1;// 静态成员不能引用类类型参数

  constructor(name: Type1, age: Type2) {
    this.name = name;
    this.age = age;
  }
}

let per = new Person('hxy', 24);
per.name = 'hxy';
per.getInfo = function (x, y) {
  return x + '' + y;
};

使用泛型的注意点

  1. 可以不使用泛型约束的情况下,尽量直接使用类型参数
  2. 尽可能少的设立类型参数,能通过已有类型参数推导出来的,就不要定义新的
  3. 泛型是用来关联多个值的类型关系的,定义单个值的类型不需要使用泛型

元组类型

Tuples,type T = [string, number],记录一些元组的注意点:

  1. 在元组类型中,也可以写可选属性,但可选元素必须在最后面,而且也会影响类型的 length
type T = [number, number, number?];
 
function fun(arg: T) {
  const [x, y, z] = arg;         
  // const z: number | undefined
 
  console.log(arg.length);
  // (property) length: 2 | 3
}
  1. 可以使用剩余元素语法,不需要写在最后面,但必须是 Array/Tuple 类型,并且有剩余元素的元组不会设置 length
type T1 = [string, number, ...boolean[]];
type T2 = [string, ...boolean[], number];
type T3 = [...boolean[], string, number];

const a: T1 = ['1', 2, true];
a.length
// (property) length: number
  1. 可以使用readonly
type T = readonly [string, number]

构造函数

和普通函数一样,构造函数可以给参数加类型注解、参数默认值、重载等,但也有不同:

  1. 构造函数上不能使用泛型
  2. 构造函数不能有返回值类型注解,因为总是会返回实例类型

TS 提供了特殊的语法,可以把一个构造函数参数转成一个同名同值的类属性,这些就被称为参数属性。我们可以在参数前面加可见性修饰符或者readonly来创建参数属性,最后这些类属性字段也会得到这些修饰符

class Params {
  constructor(
    public readonly x: number,
    protected y: number,
    private z: number
  ) {
    // No body necessary
  }
}
const a = new Params(1, 2, 3);

Getters/Setters

  • 如果get存在而set不存在,属性会被自动设置为readonly
  • 如果set参数的类型没有指定,它会被推断为get的返回类型
  • set的参数类型不一定需要和get的返回值相同

implements

类使用implements意味着需要实现一个接口(对象类型即可)

interface Pingable {
  ping(): void;
}
 
class Sonar implements Pingable {
  ping() {
    console.log("ping!");
  }
}
 
class Ball implements Pingable {
  // 类“Ball”错误实现接口“Pingable”。
  // 类型 "Ball" 中缺少属性 "ping",但类型 "Pingable" 中需要该属性。
  pong() {
    console.log("pong!");
  }
}

可以同时实现多个接口,但多个接口的同一个属性如果类型不一致,会出现冲突

type Person = {
  name: string;
}
type Child = {
  name: number;
}
 
class Stu implements Person, Child {
  // 类型“Stu”中的属性“name”不可分配给基类型“Person”中的同一属性。
  // 不能将类型“number”分配给类型“string”。
  name = 1
}

成员可见性

  • public:没有限制,默认值
  • protected:仅对子类可见
  • private:只在类内部可见,子类也不可见

需要注意的是,可见性的这些修饰符仅在类型检查中生效,转为 JS 后会失效。

静态成员

  • 使用static修饰符表示某成员是静态成员,和实例成员有区别,静态类型只能在类本身访问。
  • 静态属性可以使用可见性修饰符。
  • 类是函数,函数本身有一些属性比如name,那么设置静态属性不能起这些特殊名字,比如name、length、call等。
  • 类使用泛型时,静态成员不可用类型参数。

抽象类和成员

使用abstract修饰符表示是抽象的。抽象成员必须存在于抽象类中,抽象意味着不提供实现,并且抽象类也不能被实例化,它的作用是做为类的基类。 派生类必须要实现抽象类所有标识的属性。

abstract class Base {
  abstract getName(): string;
 
  printName() {
    console.log("Hello, " + this.getName());
  }
}
class Derived extends Base {
  getName() {
    return "world";
  }
}

内置类型

Partial 和 Required

Partial:将 T 中所有属性变为可选的 Required:将 T 中所有属性变成必选的

type Partial<T> = { [P in keyof T]?: T[P] };
type Required<T> = { [P in keyof T]-?: T[P] };

Readonly

将 T 中所有属性变为只读的

type Readonly<T> = { readonly [P in keyof T]: T[P] };

Pick

从已有对象类型中选取给定的属性及其类型,然后构建一个新的对象类型。 T 表示源对象类型,类型参数 K 提供了待选取的属性名类型,它必须为类型 T 中存在的属性。

type Pick<T, K extends keyof T> = { [P in K]: T[P] };

// 例
type T1 = { name: string; age: number; gender: string };
type T2 = Pick<T1, 'age'|'name'>
// type T1 = { age: number; name: string; }

Extract 和 Exclude

Extract:从T中提取可分配给U的类型 Exclude:从T中排出可分配给U的类型 下面的例子用到了上文提到的分发条件类型

type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;

// 例
type Type = '1' | '2' | 1 | 2;
type Example1 = Extract<Type, number>;
// type Example1 = 1 | 2
type Example2 = Exclude<Type, number>;
// type Example2 = '1' | '2'

Omit

构建一个新类型,排除T中拥有的K的属性

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

// 例
type Type = { name: string; age: number };
type Example = Omit<Type, 'name'>;
// type Example = { age: number; }

Record

构建一个新的对象类型,键的类型是 K (in K),值的类型是 T

type Record<K extends keyof any, T> = { [P in K]: T };

// 例
type keys = 'A' | 'B'
type T = Record<keys, number>
// type T = { A: number; B: number; }

ReturnType

获取函数的返回类型,借助条件类型和infer

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

// 例
const add = (num1: number, num2: number) => num1 + num2;
type returnType = ReturnType<typeof add>;
// type returnType = number

Parameters

获取函数的参数的类型,返回的一定是元祖或数组类型。借助条件类型和infer

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

// 例
const add = (...args: number[]) => args.reduce((p, c) => p + c);
type Para = Parameters<typeof add>;
// type Para = number[]
const a: Para = [1, 2];

ReadonlyArray

描述数组的属性不可以被更改,和Array用法一样,并且ReadonlyArray<Type>也有简写形式readonly Type[]。 需要注意,ArrayReadonlyArray不能双向赋值

let x: readonly number[] = [2];
let y: number[] = [1];
 
x = y; // ok
y = x; // error: 类型 "readonly number[]" 为 "readonly",不能分配给可变类型 "number[]"

字符串操作

  • Uppercase:每个字符转为大写
  • Lowercase:每个字符转为小写
  • Capitalize:首字母转为大写
  • Uncapitalize:首字母转为小写

以上类型都需要传入类型参数,一般会传入模版字面量类型,本身的要求就是要可分配给string类型。但我们不确定传入的类型是什么时可以通过交叉类型来限制。

type T = 1
type Type = Uppercase<string & T>