持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第13天,点击查看活动详情
问题来源
某天,小A正在练习类型体操,深入学习TypeScript类型操作相关知识,想要实现一个名为MyReadonly2的类型,该类型是内置工具类型Readonly的pro版本,MyReadonly2<T, K>带有两种类型的参数T和K,能够实现可选的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
小A一开始是这样实现的:
// solution 1
type Readonly<T, K extends keyof T = keyof T> = T & {
readonly [P in K]: T[P]
}
好像没什么问题,去试试看!!!
Opps! 出问题了,这是怎么回事呢?小A赶紧去翻了翻其他解法,然后他找到了这样一种解法, here:
// solution 2
type Readonly<T, K extends keyof T = keyof T> = Omit<T, K> & {
readonly [P in K]: T[P]
}
// 即:
type Readonly<T, K extends keyof T = keyof T> = Omit<T, K> & Readonly<T>
这两看上去也挺像啊,不都是两个对象类型交叉吗,为什么最初的解法就不行呢?
逝去的bug
实际上,solution 1 在TypeScript 4.4+版本及以下是可以正常工作的,而截至目前ts playground中的TypeScript版本最高已经到4.8-beta了solution 1 的这种反常行为实际上是TypeScript的一个bug,只不过说这个bug已经在这个 PR 中被修复了。
解析
将 solution 1 的问题总结一下就是,TypeScript 如何表示存在同名属性的两个对象类型的交叉类型,且其中一个对象的同名属性是只读的。
映射到具体的例子上就是这样的:
{readonly a: string} & {a: string}应该等同于{readonly a: string}还是{a: string}?
交叉类型在概念上意味着“与”,这意味着{readonly a: string} & {a: string}既是一个具有可读属性a的对象与一个具有可读和可写属性a的对象取 "交集" 的结果。这意味着最终的a属性是可读可写的,也就是说最终返回的对象应该是这样的:{a: string}.
从这个例子中可以得出,对于两个具有同名属性的对象类型形成的交叉类型,只有这两个对象中的同名属性都是只读的,最终对象中的同名属性才是只读的。
如果说只是其中一个对象类型对应的同名属性是只读的,最终对应属性的结果是可读可写的。
这时候再转头看看 solution 1, 应该能够明白为什么这种解法是错误的.
// solution 1
type Readonly<T, K extends keyof T = keyof T> = T & {
readonly [P in K]: T[P]
}
而Omit<T, K> & Readonly<T>之所以能够正常工作, 是因为Omit<T, K>已经确保了交叉类型的两个成员的同名属性不会包括K, 也就是说K不会出现在keyof Omit<T, K>中,只会出现在Readonly<T>中,且这时K是只读的,那么最终结果中的属性K也是只读的。