题目
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
}
实现
- 首先需要把传入类型的全部属性变成
readonly状态
type MyReadonly2<T, K extends keyof T> = {
readonly [key in keyof T]: T[key]
}
- 然后现在需要一个方法把不需要只读状态的属性去除掉
readonly修饰符
在数学中的交集是指设定 A,B 两个集合,由所有属于集合 A 且属于集合 B 的元素所组成的集合,比如两个集合 [1,2,3] 和 [2,3,4,5],那他们的交集就是 [2,3] 。
在 ts 中,如果是两个基本类型的交集,比如:
type Test = number & string
这样就表示 Test 类型需要是即可以是 number 又可以是 string 的类型,但在实际中,这种类型不可能存在,所以 Test 类型的结果是 never。
但如果是两个对象类型的话情况就不一样了,比如:
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。
那如果是有修饰符 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: number 与 readonly a: number 的交集应该是 a: number。
a?: number 和 a: number 的交集也是同理。
回到这题的实现中,利用这个交叉类型的规则,我们可以使用 Omit<Type, Keys> 高级类型去实现,得到需要去除 readonly 修饰符的类型,这个类型与上述类型的交集就可以得到指定属性只读的类型了。
type MyReadonly2<T, K extends keyof T> = {
readonly [key in keyof T]: T[key]
} & Omit<T, K>
- 到这一步基本已经完成了,但类型校验还有一处报错
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]
}