TS 从 0 到 1 - 泛型进阶

2023-05-17 19:52:30 浏览数 (1)

# 泛型

设计泛型是为了在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。

代码语言:javascript复制
function identity (value) {
  return value;
}

console.log(identity(2022)); // 2022

上面的代码中,定义了一个函数,这个函数接收一个参数,然后返回这个参数。对上面函数做适当调整,以支持 Number 类型的参数:

代码语言:javascript复制
function identity (value: number): Number {
  return value;
}

console.log(identity(2022)); // 2022

number 类型分配给参数和返回类型,使函数仅可用于该原始类型。但这个函数并不是可以扩展或通用的。

虽然可以使用 any 解决通用性问题,但那就失去了定义应该返回那种类型的能力,并且也使编译器失去了类型保护的作用。

为了让 identity 函数支持任意类型,可以使用泛型:

代码语言:javascript复制
function identity<T> (value: T): T {
  return value;
}

console.log(identity<Number>(2022)); // 2022

<T> 就像传递参数一样,传递需要用于特定函数调用的类型。即 Number 类型想参数 2022 一样,会出现在 T 的任何位置填充该类型。

<T> 内部的 T 称为类型变量,是期望传给 identity 函数的类型占位符。

T 代表 Type,在定义泛型时通常用作第一个类型变量名称,它可以用任何有效名称代替。除了 T 之外,还有一些常见泛型变量:

  • K - 表示对象的键类型
  • V - 表示对象的值类型
  • E - 表示元素类型

也可以引入多个类型变量:

代码语言:javascript复制
function identity <T, U> (value: T, message: U): T {
  console.log(message);
  return value;
}

console.log(identity<Number, string>(2022, 'hello')); // 2022

也可以省略尖括号,让编译器自动选择类型,让代码更简洁:

代码语言:javascript复制
function identity <T, U> (value: T, message: U): T {
  console.log(message);
  return value;
}

console.log(identity(2022, 'hello')); // 2022

# 泛型接口

代码语言:javascript复制
interface Identities<V, M> {
  value: V;
  message: M;
}

function identity<T, U> (value: T, message: U): Identities<T, U> {
  console.log(value   ": "   typeof (value));
  console.log(message   ": "   typeof (message));
  let identities: Identities<T, U> = {
    value,
    message
  };
  return identities;
}

console.log(identity(2022, 'hello'));
// 2022: number
// hello: string
// { value: 2022, message: 'hello' }

# 泛型类

在类中使用泛型,只要在类名后面,使用 <T, ...> 的语法定义任意多个类型变量:

代码语言:javascript复制
interface GenericInterface<U> {
  value: U;
  getIdentity: () => U;
}

class IdentityClass<T> implements GenericInterface<T> {
  value: T

  constructor(value: T) {
    this.value = value;
  }

  getIdentity(): T {
    return this.value;
  }
}

const myNumberClass = new IdentityClass<Number>(2022);
console.log(myNumberClass.getIdentity()); // 2022

const myStringClass = new IdentityClass<string>('Cellinlab');
console.log(myStringClass.getIdentity()); // Cellinlab

泛型类可确保在整个类中一致地使用指定的数据类型。

# 泛型使用时机

  • 当函数、接口或类将处理多种数据类型时(为了通用)
  • 当函数、接口或类在多个地方使用该数据类型时(为了一致)

# 泛型约束

泛型约束用于限制每个类型变量接受的类型数量。

# 确保属性存在

有时,希望类型变量对应的类型上存在某些属性,除非显式地将特定属性定义为类型变量,否自编译器不会知道这些属性是否存在。

代码语言:javascript复制
function identity<T>(arg: T): T {
  console.log(arg.length); // Error: T doesn't have .length
  return arg;
}

可以让类型变量 extends 一个含有需要的属性的接口:

代码语言:javascript复制
interface Length {
  length: number;
}

function identity<T extends Length>(arg: T): T {
  console.log(arg.length); // Now we know it has a .length property, so no more error
  return arg;
}

# 检查对象上的键是否存在

keyof 用于获取某种类型的所有键,其返回类型是联合类型:

代码语言:javascript复制
interface Person {
  name: string;
  age: number;
  location: string;
}

type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[]; // "length" | "push" | "pop" | "concat" | ...
type K3 = keyof { [x: string]: Person }; // string | number

使用 keyof 可以获取指定类型的所有键,然后结合 extends 约束,限制输入的属性包含在 keyof 返回的联合类型中。

代码语言:javascript复制
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

enum Difficulty {
  Easy,
  Medium,
  Hard
}

let tsInfo = {
  name: 'TypeScript',
  supersetOf: 'JavaScript',
  difficulty: Difficulty.Medium
}

let difficulty: Difficulty = getProperty(tsInfo, 'difficulty'); // Medium
let supersetOf: string = getProperty(tsInfo, 'superset_of'); // Error: Argument of type '"superset_of"' is not assignable to parameter of type '"name" | "supersetOf" | "difficulty"'

通过 K extends keyof T 确保参数 key 一定是对象中含有的键,这样就不会发生运行时错误。

# 泛型参数默认类型

可以为泛型中的类型参数指定默认类型,当使用泛型时没有在代码中直接指定参数类型参数,从实际值参数中无法推断出类型时,这个默认类型就会起作用。

语法 <T=默认类型>

代码语言:javascript复制
interface A<T=string> {
  name: T;
}

const strA: A = { name: 'Cellinlab' };
const numA: A<number> = { name: 2022 };

泛型参数的默认类型遵循以下规则:

  • 有默认类型的类型参数被认为是可选的
  • 必选的类型参数不能在可选的类型参数之后
  • 如果类型参数有约束,类型参数的默认类型必须满足约束
  • 当指定类型实参时,只需要指定必选类型参数的类型实参,未指定的类型参数会被解析为默认类型
  • 一个被现有类或接口合并的类或者接口的声明可以为现有类型参数引入默认类型
  • 一个被现有类或接口合并的类或者接口的声明可以引入新的类型参数,只要它指定了默认类型

# 泛型条件类型

条件类型可以根据某些条件得到不同的类型,这里的条件值类型兼容性约束。

虽然之前代码中使用了 extends 关键字,但也不一定要强制满足继承关系,而是检查是否满足结构兼容性。

代码语言:javascript复制
// 若 T 能赋值给 U 那么类型是 X,否则是 Y
T extends U ? X : Y

通常还会结合 infer 关键字,实现类型抽取:

代码语言:javascript复制
interface Dictionary<T = any> {
  [key: string]: T;
}

type StrDict = Dictionary<string>;

type DictMember<T> = T extends Dictionary<infer U> ? U : never;
type StrDictMember = DictMember<StrDict>; // string

never 类型表示那些永不存在的值的类型。如,never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头表达式的返回值类型。

没有类型是 never 的子类型或可以赋值给 never 类型(除了 never 本身)。

利用条件类型和 infer 关键字,还可以方便地实现获取 Promise 对象的返回值类型。

代码语言:javascript复制
async function stringPromise() {
  return "Hello, Cellinlab!";
}

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

async function personPromise() {
  return {
    name: "Cell",
    age: 18
  } as Person;
}

type PromiseType<T> = (args: any[]) => Promise<T>;
type UnPromisify<T> = T extends PromiseType<infer U> ? U : never;

type extractStringPromise = UnPromisify<typeof stringPromise>; // string
type extractPersonPromise = UnPromisify<typeof personPromise>; // Person

# 泛型工具类型

# Partial

Partial<T> 用于将某个类型里的属性全部变为可选项 ?

  • 定义
代码语言:javascript复制
type Partial<T> = {
  [P in keyof T]?: T[P];
}
// 通过 keyof T 拿到 T 的所有属性名
// 然后使用 in 进行遍历,将值赋给 P
// 最后通过 T[P] 取得相应的属性值
// 中间的 ? 用于将所有属性变为可选

  • 实例
代码语言:javascript复制
interface Todo {
  title: string;
  description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return {
    ...todo,
    ...fieldsToUpdate
  };
}

const todo1 = {
  title: "Learn TypeScript",
  description: "Learn TypeScript",
};

const todo2 = updateTodo(todo1, {
  description: "Learn TypeScript everyday"
});

# Record

Record<K extends keyof any, T> 作用是将 K 中所有的属性的值转化为 T 类型

  • 定义
代码语言:javascript复制
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

  • 示例
代码语言:javascript复制
interface PageInfo {
  title: string;
}

type Page = "home" | "about" | "contact";

const x: Record<Page, PageInfo> = {
  about: { title: "about" },
  contact: { title: "contact" },
  home: { title: "home" },
};

# Pick

Pick<T, K extends keyof T>的作用是将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型。

  • 定义
代码语言:javascript复制
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
}

  • 示例
代码语言:javascript复制
interface Todo {
  title: string,
  description: string,
  completed: boolean,
}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
  title: "Learn TypeScript",
  completed: false,
};

# Exclude

Exclude<T, U> 用于将某个类型中属于另一个的类型移除掉。

  • 定义
代码语言:javascript复制
// 如果 T 能赋值给 U,那么就会返回 never 类型,否则返回 T 类型
// 最终实现的效果就是将 T 中某些属于 U 的类型移除掉
type Exclude<T, U> = T extends U ? never : T;

  • 示例
代码语言:javascript复制
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number

# ReturnType

ReturnType<T> 用于获取函数 T 的返回类型。

  • 定义
代码语言:javascript复制
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

  • 示例
代码语言:javascript复制
type T0 = ReturnType<() => string>; // string
type T1 = ReturnType<(s: string) => void>; // void
type T2 = ReturnType<<T>() => T>; // {}

# 使用泛型创建对象

# 构造签名

有时,泛型类需要基于传入的泛型 T 来创建其类型相关的对象。

在 TypeScript 中,可以使用 new 关键字来描述一个构造函数:

代码语言:javascript复制
interface Point {
  new (x: number, y: number): Point;
}

new (x: number, y: number) 称为构造签名,语法是:

ConstructSignature = new TypeParametersopt (ParameterListopt) TypeAnnotationopt

  • TypeParametersopt - 可选的类型参数
  • ParameterListopt - 可选的参数列表
  • TypeAnnotationopt - 可选的类型注解
代码语言:javascript复制
new C
new C (...)
new C <...> (...)

# 构造函数类型

构造函数类型:

  • 包含一个或多个构造签名的对象类型
  • 可以使用构造函数类型字面量或包含构造签名的对象类型字面量来编写

构造函数类型字面量的形式:

代码语言:javascript复制
new <T1, T2, ...> (p1, p2, ...) => R

对象类型字面量:

代码语言:javascript复制
{
  new <T1, T2, ...> (p1, p2, ...): R;
}

示例

代码语言:javascript复制
// 构造函数类型字面量
new (x: number, y: number) => Point;

// 对象类型字面量
{
  new (x: number, y: number): Point;
}

# 构造函数类型的应用

代码语言:javascript复制
interface Point {
  x: number;
  y: number;
}

interface PointConstructor {
  new (x: number, y: number): Point;
}

class Point2D implements Point {
  readonly x: number;
  readonly y: number;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

function newPoint(
  pointConstructor: PointConstructor,
  x: number,
  y: number
): Point {
  return new pointConstructor(x, y);
}

const point: Point = newPoint(Point2D, 1, 2);

# 使用泛型创建对象

代码语言:javascript复制
class GenericCreator<T> {
  create<T>(c: { new (): T }): T {
    return new c();
  }
}

0 人点赞