likes
comments
collection
share

常考 TS 手写--妈妈再也不用担心我的TS了

作者站长头像
站长
· 阅读数 44

随着 TS 在工作中使用的越来越广泛,面试的时候面试官也都会加上一两个 TS 的问题来了解候选人对于 TS 的熟悉程度,其中就有不少手写题目,比如笔者在字节的一次二面,面试官就问到了我如何实现一个 Pick,在蔚来的一面,面试官问到了我如何实现一个 Omit。

接下来,让我们一起看看有哪些常见 TS 手写题吧。

手写 pick

Pick<T, K> ==> 从类型 T 中选出属性 K,构造成一个新的类型。

前置知识:type: 作用就是给类型起一个新名字,支持基本类型联合类型、元组及其他任何你需要的手写类型,常用于联合类型,与接口一样,用来描述对象或函数的类型。

手写实现:

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

使用示范:

// 从 User 中挑出 stature 来定义类型
interface User {
  stature: number
  age: number
}
const people: Pick<User, "stature"> = {
  stature: 185
}

手写 Exclude

Exclude 是 TypeScript 的一种类型操作符,用于从类型 T 中排除掉指定的类型 K。其定义为:

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

这个定义中,使用了 TypeScript 的条件类型,它主要的义务是允许我们在类型系统中进行运算和推断。

具体来说,在这个条件类型中,我们首先声明了一个泛型类型参数 TK。然后,使用了 extends 关键字将 TK 进行对比。如果 TK 的子类型,则返回 never 类型;否则返回 T 类型。

为什么要返回 never 呢?因为 never 代表的是永远不可能出现的类型,相当于一个空集合。这样,在使用 Exclude 操作符时,我们就可以将 T 中与 K 相同的类型排除掉,从而得到一个新的类型。

例如,假设我们有以下两个类型:

type A = "a" | "b" | "c" | "d";
type B = "b" | "d" | "e" | "f";

我们可以使用 Exclude 操作符将 B 中包含的类型从 A 中排除掉,得到一个新的类型 C

type C = Exclude<A, B>; // "a" | "c"

因为 A 中包含了四个类型,而 B 中包含了 "b""d",所以将这两个类型从 A 中排除后,剩余的类型就是 "a""c"。因此,新的类型 C 就是 "a" 或者 "c"

手写 partial

Partial 是 TypeScript 内置的一个类型操作符,它用于将某个类型中每个属性设置为可选属性,这表示这些属性的值可以是 undefined 或者省略。

下面是 Partial 的代码:

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

这个定义使用了 TypeScript 中的映射类型(Mapped Types)和索引访问类型(Index Access Types)。

首先,声明一个泛型类型 T 作为待操作的类型。然后,使用了映射类型语法,声明一个新类型,其属性名为 P,该属性名必须是 T 的属性名之一,属性值为该属性名在 T 类型中对应的类型。并且使用 ? 符号来将所有属性设置为可选属性。

该类型操作符的作用是让某个类型中的每个属性都变成可选属性,从而灵活地处理类型的使用,特别是在一些属性不是必需的场景下。

以下是一个使用 Partial 操作符的示例:

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

type PartialPerson = Partial<Person>;

let person1: PartialPerson = { name: "Alice" }; // age 是可选属性,值默认为 undefined
let person2: PartialPerson = { }; // name 和 age 都是可选属性,值默认为 undefined
let person3: Partial<Person> = { name: "Bob", age: 20 }; // 和 Person 一样,都是必选属性

上面的示例中,我们先声明了一个 Person 接口,然后使用 Partial 操作符将其转化为可选类型 PartialPerson,接着创建三个可选类型的变量 person1person2person3。在 person1 中,只设置了 name 属性,而 age 属性是可选的,其值默认为 undefined;在 person2 中,没有设置任何属性,两个属性都是可选的,其值默认为 undefined;在 person3 中,设置了 nameage 两个必选属性,其值和 Person 类型一致。

手写 Omit

Omit 是 TypeScript 的一种类型操作符,用于从类型 T 中删去指定的属性 K。其定义为:

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

这个定义中,使用了 TypeScript 的类型推导机制和 Exclude 操作符。

首先,我们声明了两个泛型类型参数 TK,其中 T 表示要操作的类型,K 表示要删去的属性名。

然后,使用了 keyof T 操作符,获取了 T 类型的所有属性名组成的联合类型。接着,使用 Exclude 操作符将 K 从这个联合类型中排除掉,得到一个新的属性名联合类型。最后,使用 Pick 操作符从 T 类型中选取删去了属性 K 后的属性集合。

例如,假设我们有以下类型:

interface Person {
  name: string;
  age: number;
  address: {
    city: string;
    street: string;
  };
}

type PersonNameAndAge = Omit<Person, "address">;

PersonNameAndAge 将会是一个新的类型,它只包含 Person 中的 nameage 两个属性。可以通过以下方式检查它的类型:

const person: PersonNameAndAge = { name: "Alice", age: 30 };
person.name = "Bob"; // 可以修改
person.address = { city: "Shanghai", street: "Nanjing Road" }; // 报错,address 属性不存在

因此,Omit 操作符可以方便地帮助我们从一个类型中删去指定的属性,得到一个新的类型。

手写 Readonly

Readonly 是 TypeScript 内置的一个类型操作符,它用于将某个类型中每个属性设置为只读属性,这表示这些属性的值不能被修改。

下面是 Readonly 的代码:

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

这个定义使用了 TypeScript 中的映射类型(Mapped Types)和索引访问类型(Index Access Types)。

首先,声明一个泛型类型 T 作为待操作的类型。然后,使用了映射类型语法,声明一个新类型,其属性名为 P,该属性名必须是 T 的属性名之一,属性值为该属性名在 T 类型中对应的类型。并且使用 readonly 关键字将属性设置为只读属性。

该类型操作符的作用是保护类型中的属性免于被错误的修改,特别是防止在多个地方引用该类型中的同一个属性时产生冲突。

以下是一个使用 Readonly 操作符的示例:

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

type ReadonlyPerson = Readonly<Person>;

let person: ReadonlyPerson = { name: "Alice", age: 30 };
person.name = "Bob"; // 报错,因为 name 是只读属性
person.age = 40; // 报错,因为 age 是只读属性
person = { name: "Carol", age: 50 }; // 可以修改整体属性值

上面的示例中,我们先声明了一个 Person 接口,然后使用 Readonly 操作符将其转化为只读类型 ReadonlyPerson,接着创建一个只读类型的变量 person,并赋初值为 { name: "Alice", age: 30 }。由于 ReadonlyPerson 中的每个属性都是只读属性,所以我们不能修改 person 中的任何属性。如果要修改整个数据对象,我们需要分配一个新对象来覆盖 person

手写 Required

Required 是 TypeScript 内置的一个类型操作符,它用于将某个类型的所有可选属性都转换为必选属性。

下面是 Required 的代码:

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

这个定义使用了 TypeScript 中的映射类型(Mapped Types)和索引访问类型(Index Access Types)。

首先,声明一个泛型类型 T 作为待操作的类型。然后,使用了映射类型语法,声明一个新类型,其属性名为 P,该属性名必须是 T 的属性名之一,属性值为该属性名在 T 类型中对应的类型。并在属性名前使用 -? 符号来将所有属性设置为必选属性。

该类型操作符的作用是保证某个类型中的每个属性都必须有值,从而避免在使用该类型的地方出现意料之外的错误或者异常情况。

下面是一个使用 Required 操作符的示例:

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

type RequiredPerson = Required<Person>;

let person: RequiredPerson = { name: "Alice", age: 30 };
person.name = "Carol"; // 正常
person.age = 50; // 正常
person = { }; // 报错,因为 name 和 age 是必选属性

上面的示例中,我们先声明了一个 Person 接口,然后使用 Required 操作符将其转化为必选类型 RequiredPerson,接着创建一个必选类型的变量 person,并赋初值为 { name: "Alice", age: 30 }。由于 RequiredPerson 中的每个属性都是必选属性,所以我们必须设置 person 中的所有属性。如果我们忘记设置某个属性,或者设置错误的属性名,就会在编译期间发生错误,避免出现运行期错误。

转载自:https://juejin.cn/post/7239296984984862781
评论
请登录