【TypeScript 学习】Typehero - Readonly 2

187 阅读3分钟

题目

Implement a generic MyReadonly2<T, K> which takes two type argument T and K.
实现一个泛型 MyReadonly2<T, K> ,它接受两个类型参数 T 和 K .

K specify the set of properties of T that should set to Readonly. When K is not provided, it should make all properties readonly just like the normal Readonly<T>.
K 指定应设置为 Readonly 的 T 属性集。如果 K 未提供,则应使所有属性都像正常 Readonly<T> 一样只读。

For example 例如

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>>,
    Expect<Alike<MyReadonly2<Todo2, 'description' >, Expected>>,
]

// @ts-expect-error
type error = MyReadonly2<Todo1, 'title' | 'invalid'>

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
}

实现

  1. 首先需要把传入类型的全部属性变成 readonly状态
type MyReadonly2<T, K extends keyof T> = {
    readonly [key in keyof T]: T[key]
}
  1. 然后现在需要一个方法把不需要只读状态的属性去除掉 readonly 修饰符

在数学中的交集是指设定 A,B 两个集合,由所有属于集合 A 且属于集合 B 的元素所组成的集合,比如两个集合 [1,2,3][2,3,4,5],那他们的交集就是 [2,3] 。 在 ts 中,如果是两个基本类型的交集,比如:

type Test = number & string

这样就表示 Test 类型需要是即可以是 number 又可以是 string 的类型,但在实际中,这种类型不可能存在,所以 Test 类型的结果是 never

image.png

但如果是两个对象类型的话情况就不一样了,比如:

type TypeV1 = {
  a: number
  c: number
}

type TypeV2 = {
  b: string
  c: string
}

type TypeV3 = TypeV1 & TypeV2

type TypeV4 = {
  [K in keyof TypeV3]: TypeV3[K]
}

对于对象类型来说,如果需要一个类型又要可以属于 { a: number },又要属于 { b: string },那么就只能同时包含这两种类型,而属性 c 跟上文说到的原因一样,基础类型不可能存在又属于 number 且属于 string 的类型,所以结果也是 never

image.png

那如果是有修饰符 readonly 的情况呢?

type TypeV1 = {
  a: number
}

type TypeV2 = {
  readonly a: number
  b: number
}

根据上述的规则,如果一个类型需要又属于 a: number 且又属于 readonly a: number,这时候这个类型就不能是 readonly a: number 了,因为如果 a 属性是 number 类型但不一定是 readonly 状态,而 readonly a: number 包含 a: number ,所以 a: numberreadonly a: number 的交集应该是 a: numbera?: numbera: number 的交集也是同理。

image.png

回到这题的实现中,利用这个交叉类型的规则,我们可以使用 Omit<Type, Keys> 高级类型去实现,得到需要去除 readonly 修饰符的类型,这个类型与上述类型的交集就可以得到指定属性只读的类型了。

type MyReadonly2<T, K extends keyof T> = {
  readonly [key in keyof T]: T[key]
} & Omit<T, K>
  1. 到这一步基本已经完成了,但类型校验还有一处报错
Expect<Alike<MyReadonly2<Todo1>, Readonly<Todo1>>>

这里 MyReadonly2 的第二个参数没有传值,想要实现的就是 Readonly<Type> 的效果,把所有属性变成只读状态。解决办法也很简单,只需要给第二个参数一个默认值就可以了。

type MyReadonly2<T, K extends keyof T = keyof T> = {
  readonly [key in keyof T]: T[key]
} & Omit<T, K>

如果不想要使用 Omit 高级类型去实现,也可以手敲 Omit 实现:

type MyReadonly2<T, K extends keyof T = keyof T> = {
	readonly [key in keyof T]: T[key]
} & {
	[key in keyof T as key extends K ? never : key]: T[key]
}