常考 TS 手写--妈妈再也不用担心我的TS了
随着 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 的条件类型,它主要的义务是允许我们在类型系统中进行运算和推断。
具体来说,在这个条件类型中,我们首先声明了一个泛型类型参数 T
和 K
。然后,使用了 extends
关键字将 T
与 K
进行对比。如果 T
是 K
的子类型,则返回 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
,接着创建三个可选类型的变量 person1
、person2
、person3
。在 person1
中,只设置了 name
属性,而 age
属性是可选的,其值默认为 undefined
;在 person2
中,没有设置任何属性,两个属性都是可选的,其值默认为 undefined
;在 person3
中,设置了 name
和 age
两个必选属性,其值和 Person
类型一致。
手写 Omit
Omit
是 TypeScript 的一种类型操作符,用于从类型 T
中删去指定的属性 K
。其定义为:
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
这个定义中,使用了 TypeScript 的类型推导机制和 Exclude
操作符。
首先,我们声明了两个泛型类型参数 T
和 K
,其中 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
中的 name
和 age
两个属性。可以通过以下方式检查它的类型:
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