type-challenges:Readonly 2

52 阅读3分钟

Readonly 2

问题描述

实现一个泛型MyReadonly2<T, K>,它带有两种类型的参数TK

类型 K 指定 T 中要被设置为只读 (readonly) 的属性。如果未提供K,则应使所有属性都变为只读,就像普通的Readonly<T>一样。

例如

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
// ============= Test Cases =============
import type { Alike, Expect } from './test-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
}
​
// ============= Your Code Here =============
// 答案
type MyReadonly2<T, K extends keyof T = keyof T> = {
  [p in keyof T as p extends K ? never : p]: T[p]
} & {
  readonly [p in K]: T[p]
}
​
// 错误答案1
// type MyReadonly2<T, K extends keyof T = keyof T> = Pick<T,Exclude<keyof T, K>> & {readonly [P in K]: T[K]}// 错误答案2
// type MyExlude<T, K> = T extends K ? never : T
// type MyReadonly2<T, K extends keyof T = keyof T> = {readonly [k in K]: T[k]} & { [k in MyExlude<keyof T, K>]: T[k] }
// 错误答案3
// type MyReadonly2<T, K extends keyof T = keyof T> = {[P in Exclude<keyof T, K>]: T[P]} & {readonly [P in K]: T[P]}

这道题实际上和上一道 Omit 是有联系的,如何实现对象中指定的属性变为只读属性,这里的基本思路有两个。

第一种,将当前第二个泛型传入的属性从对象中排除(使用 Omit),并且将当前属性变为只读属性,使用联合类型拼接返回。

第二种:将除了当前类型第二个参数传入的属性挑出来(使用 Pick),并且将当前属性变为只读属性,使用联合类型拼接返回。

先看第一种,排除当前第二个泛型参数传入的属性,这里有两种方式,再上一题 Omit 中也写过,实际上,这两种方式并不完全一致,举例来说:


interface Todo2 {
  readonly title: string
  description?: string
  completed: boolean
}
​
type MyOmit1<T, K extends keyof T = keyof T> = {
  [P in Exclude<keyof T, K>]: T[P]
}
type MyOmit2<T, K extends keyof T = keyof T> = {
  [p in keyof T as p extends K ? never : p]: T[p]
}
​
type Answer1 = MyOmit1<Todo2, 'description'>= {
    title: string;
    completed: boolean;
}
​
type Answer2 = MyOmit2<Todo2, 'description'>={
    readonly title: string;
    completed: boolean;
}

MyOmit1 的定义中,通过 Exclude<keyof T, K> 排除了指定的键,然后直接保留剩余键对应的值类型,没有保留属性的修饰符,所以 readonly 修饰符就没有了。这是 TypeScript 的设计规定。通过索引签名或使用 Exclude<keyof T, K> 来创建新类型时,不会自动保留属性的修饰符,如 readonly。TypeScript 的核心原则之一是对值所具有的结构进行类型检查。当使用类似 MyOmit1 中的方式通过排除某些键来创建新类型时,重点在于保留剩余键及其对应的值类型,而不是属性修饰符。这样可以更灵活地根据需要创建具有特定结构的类型。

而在 MyOmit2 的定义中,通过 as 操作符进行条件判断来重新映射键,保留了原始属性的修饰符等特性。所以 readonly 得以保留。如 MyOmit2 中通过 as 操作符进行条件判断来重新映射键的方式。这样的设计使得开发者可以根据具体的需求和场景,选择合适的方式来操作和创建类型,以满足不同的编程需求。在实际开发中,需要根据具体情况来决定是否保留属性修饰符,并选择相应的方法来定义类型。如果想要保留 readonly 等修饰符,就可以采用类似 MyOmit2 的方式进行类型定义。

综上,想让第四个测试用例通过,就只能使用 [p in keyof T as p extends K ? never : p]: T[p] 的方式。至于错误答案2和错误答案3,也是使用了 Exclude,所以都不可行。

第二种:这里使用 Pick 时,需要排除 当前的属性,得到其余的属性,依旧需要用到Exclude,所以这种方法也不可行。