我认为我和你一样有同样的问题。简短回答是,我不认为这是一个很好的DeepReadonly<T>
实现,没有冒犯写这个代码的人的意思。
keyof T extends never
表示 T
类型没有已知的键,因为 keyof
运算符 生成一个已知键的 联合类型,而 never
类型 是 TypeScript 的 底部类型,它是一种根本没有值的类型。这意味着keyof T extends never
的行为如下:type Hmm<T> = keyof T extends never ? true : false
type X1 = Hmm<{ a: string }> // false, "a" is a known key
type X2 = Hmm<{}> // true, there are no known keys
type X3 = Hmm<object> // true, there are no known keys
type X4 = Hmm<string> // false, there are keys like "toUpperCase"
type X5 = Hmm<
{ a: string } | { b: string }
> // true, unions with no common keys have no known keys
keyof T extends never
并不是这样做的。例如:type DeepReadonly<T> = keyof T extends never
? T
: { readonly [K in keyof T]: DeepReadonly<T[K]> };
type Z = DeepReadonly<{ a: string } | { b: string }>
// type Z = {a: string} | {b: string} OOPS
declare const z: Z;
if ("a" in z) {
z.a = "" // no error, not readonly
}
由于我们传入了一个联合类型,编译器将其键值视为never
,突然间我们没有了任何readonly
。糟糕。
“DeepReadonly<T>”的“正确”定义可能只是
type DeepReadonly<T> =
{ readonly [K in keyof T]: DeepReadonly<T[K]> };
映射类型已经通过返回输入来“跳过”原始类型,并且它们自动分布在联合类型上,因此您无需自己检查这些内容:
type Look<T> = { [K in keyof T]: 123 };
type Y1 = Look<{ a: string }> // {a: 123}
type Y2 = Look<string> // string
type Y3 = Look<{ a: string } | { b: string }>
// Look<{ a: string; }> | Look<{ b: string; }>
DeepReadonly
,我们也可以正确处理联合类型:type Z = DeepReadonly<{ a: string } | { b: string }>
// type Z = DeepReadonly<{ a: string; }> | DeepReadonly<{ b: string; }>
declare const z: Z;
if ("a" in z) {
z.a = "" // error! Cannot assign to 'a' because it is a read-only property
}
如果您确实想要检查对象和原始类型,最好使用object
类型:
type DeepReadonly<T> = T extends object ?
{ readonly [K in keyof T]: DeepReadonly<T[K]> } : T;
这类似于没有检查的类型:T extends object ? ... : T
是一个distributive conditional type,因此它自动拆分联合类型,处理它们,并将它们重新组合:
type Z = DeepReadonly<{ a: string } | { b: string }>
// type Z = { readonly a: string; } | { readonly b: string; }
尽管 IntelliSense 的快速信息显示方式不同,但这与先前的类型相同。
type DeepReadonly<T> = T extends Function ? T : { readonly [K in keyof T]: DeepReadonly<T[K]> };
。当然,具体使用情况会推动定义;即使是内置的实用类型如Readonly<T>
也会对函数进行奇怪的处理(例如Readonly<() => 22>
会产生{}
而不是() => 22
),因此目前我认为在没有特定使用要求的情况下,“DeepReadonly”的“正确”定义仍然可能是我发布的那个定义。 - jcalz
keyof T
什么情况下会为空值呢? - dipea