[ TS 类型体操] 初体验之 Readonly、Readonly 2 与 Deep Readonly

363 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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;
}

根据以上代码,我们可以总结出如下要点

  1. K 中的元素必须是 T 中的属性,若不是则抛出错误
  2. 筛选 T 中包含在 K 中的属性,转化为 readonly
  3. 当 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>.

  1. Pick<T, K> 返回在 K 中的 T 属性
  2. Omit<T, K> 剔除在 T 中的 K 属性,并返回
  3. 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]
}

总结

  1. 只读属性
  1. 使用 keyof 对类型进行提取
  2. 使用 extends 对类型进行限制的两种方法
  1. 使用 T[P] 来获得类型
  1. as never 来将 union 中的元素去除
  1. 使用 in 来进行遍历

这是我自己的练题仓库,里面会总结我在练习类型体操中遇到的相关问题以及相关知识点,如果这对你有帮助的话就给我一个 star 吧😄