持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 1 天,点击查看活动详情
前言
在上一篇文章中,我们了解到了如何在一个对象中使用 in 来遍历 K 并通过使用 T[P] 来获得类型。以及使用 keyof 来对类型进行提取。并且利用 as never 来将一个 union 中的类型进行删除。
在这一篇文章中,我们将继续实现 type-challenges 中的 Readonly 以及他的拓展题 Readonly 2 与 Deep Readonly
Readonly
在这道题中,我们将实现一个内置泛型 Readonly< T >,他的作用是将 T 中的所有元素转换为 readonly,只允许读而不允许重新赋值。 问题链接
interface Todo {
title: string
description: string
}
const todo: MyReadonly<Todo> = {
title: "Hey",
description: "foobar"
}
todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property
测试用例如下
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<MyReadonly<Todo1>, Readonly<Todo1>>>,
]
interface Todo1 {
title: string
description: string
completed: boolean
meta: {
author: string
}
}
在 TS 中,我们如果想让一个对象中的属性变为只读的, 我们可以在属性前边加上 readonly 就可以达到预期的效果。
所以我们可以利用上一篇文章学到的 keyof 来获取到 T 的每一个属性,之后再利用 in 来遍历每一个的属性,使用 T[P] 来获取对应属性的值,之后在前面加上一个 readonly 就可以实现这个功能
由于这道题于上一道题 Pick 解题思路相对重合,所以在此不再细分解题思路。直接在此给出答案,如果有不明白的地方,请参考上一篇文章 [ TS 类型体操] 初体验之 Pick 与 Omit
type MyReadonly<T> = {
readonly [P in keyof T]: T[P]
}
Readonly 2
在这道题中,我们将实现一个泛型 MyReadonly2<T, K>。他将传入两个参数 T 与 K,其中 K 必须是 T 中的属性,且在 K 中的元素将会被转为 readonly 属性。如果 K 没有传值的话,则将 T 中所有属性转换为 readonly。 问题链接
interface Todo {
title: string
description: string
completed: boolean
}
const todo: MyReadonly2<Todo, 'title' | 'description'> = {
title: "Hey",
description: "foobar",
completed: false,
}
todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property
todo.completed = true // OK
测试用例如下
import type { Alike, Expect } from '@type-challenges/utils'
type cases = [
Expect<Alike<MyReadonly2<Todo1>, Readonly<Todo1>>>,
Expect<Alike<MyReadonly2<Todo1, 'title' | 'description'>, Expected>>,
Expect<Alike<MyReadonly2<Todo2, 'title' | 'description'>, Expected>>,
]
interface Todo1 {
title: string
description?: string
completed: boolean
}
interface Todo2 {
readonly title: string
description?: string
completed: boolean
}
interface Expected {
readonly title: string
readonly description?: string
completed: boolean
}
当我们没有思路时,我们可以试着将先将对应需求的 JS 代码写出,在提取要点,最后转化为 TS 代码。
所以根据以上需求,我们可以很容易的写出对应的 JS 代码
function readonly2(T, K) {
let result = {};
for (let i = 0; i < K.length; i++) {
if (!T.includes(K[i])) {
throw new Error("Key not found");
}
}
for(let obj in T) {
if (K.includes(obj)) {
result["readonly" + obj] = T[obj];
} else {
result[obj] = T[obj];
}
}
return result;
}
根据以上代码,我们可以总结出如下要点
- K 中的元素必须是 T 中的属性,若不是则抛出错误
- 筛选 T 中包含在 K 中的属性,转化为 readonly
- 当 K 中不存在元素时,直接遍历整个 T,并将其转化为 readonly
根据以上三点,我们来一步步完成整个题目
K 中的元素必须是 T 中的属性
根据上一篇文章中提到的,我们可以很容易的想到可以使用 keyof 来对 T 属性进行提取,再使用 extends 来对 K 进行约束
type MyReadonly2<T, K extends keyof T> = {}
筛选 T 中包含在 K 中的属性,转化为 readonly
在这个要点中,我们需要遍历 T 中的所有元素,并通过判断来剔除掉不在 K 中的。但在 TS 中,我们并没有 if 来进行判断。所以,我们不妨换一个思路,我们遍历两个对象,一个对象中是 K 中的元素,一个是 T 剔除 K 中元素后剩余的属性。之后再将两个对象进行合并,这样我们就可以实现相关功能。
type MyReadonly2<T, K extends keyof T> = {
readonly [P in K]: T[P]
} & {
[P in keyof T as P extends K ? never : P]: T[P]
}
在这一步骤中,我们首先对 K 中的元素进行遍历,之后再对 T 进行遍历的过程中,使用 extends 来对 K 中的元素进行剔除。最后使用 & 来讲两个对象合并。最终实现整个功能
当 K 中不存在元素时,将 T 中属性转化为 readonly
在这一步骤中,当 K 中没有值时,将整个 T 转化为 readonly。在 JS 中,我们可以通过设置一个默认值的方式来进行处理。而我们知道,TS 是 JS 的一个超集,所以我们也可以也可以利用这一特性来为 K 设置一个初始值
type MyReadonly2<T, K extends keyof T = keyof T> = {
readonly [P in K]: T[P]
} & {
[P in keyof T as P extends K ? never : P]: T[P]
}
至此,readonly 2 我们已经完成但我们不妨想一想,我们可不可以通过别的方法来重写这个题目,试着利用 TS 内置的泛型来解决它?
type MyReadonly2<T, K extends keyof T = keyof T> = Readonly<Pick<T, K>> & Omit<T, K>;
在上一篇文章中我们实现了内置泛型 Pick<T, K> 与 Omit<T, K>,以及在上一题中我们实现了内置泛型 Readonly<T>.
- Pick<T, K> 返回在 K 中的 T 属性
- Omit<T, K> 剔除在 T 中的 K 属性,并返回
- Readonly<T> 将 T 中的属性转换为 readonly
所以利用以上内置泛型,我们可以轻松的实现 MyReadonly2 这个泛型,在传入 T 时,只需将 Pick 与 Omit 使用 & 进行连接即可
Deep Readonly
在这道题目中,我们将实现一个 Deep Readonly,可以对深层次的对象也进行 Readonly 处理。 问题链接
type X = {
x: {
a: 1
b: 'hi'
}
y: 'hey'
}
type Expected = {
readonly x: {
readonly a: 1
readonly b: 'hi'
}
readonly y: 'hey'
}
type Todo = DeepReadonly<X> // should be same as `Expected`
根据以上信息我们可以很容易的想到可以利用递归来解决这个问题,首先对 X 进行遍历,对 T[P] 进行判断,如果是一个对象的话,则递归的调用 DeepReadonly<T[P]>。值得注意的是,在进行条件判断时,我们需要先将 Function 进行剔除,因为 Function 也是 Object 的子类型。
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends Function
? T[P]
: T[P] extends Object
? DeepReadonly<T[P]>
: T[P]
}
总结
- 只读属性
- 使用 keyof 对类型进行提取
- 使用 extends 对类型进行限制的两种方法
- 使用 T[P] 来获得类型
- as never 来将 union 中的元素去除
- 使用 in 来进行遍历
这是我自己的练题仓库,里面会总结我在练习类型体操中遇到的相关问题以及相关知识点,如果这对你有帮助的话就给我一个 star 吧😄