"extends never" 是用来做什么的?

15

这里是 DeepReadonly 的实现(取自此处,它是此挑战的解
type DeepReadonly<T> = keyof T extends never
  ? T
  : { readonly [k in keyof T]: DeepReadonly<T[k]> };

Great, I'm here to help! What do you need me to translate?

https://www.typescriptlang.org/docs/handbook/2/conditional-types.html - jonrsharpe
'never' 表示可以为空值。Extends 意味着接口扩展另一个接口或类型。 - Steve Tomlin
@jonrsharpe 我对条件类型的基本概念是理解的,只是不太明白这个特定示例是如何工作的。 - dipea
@SteveTomlin,keyof T 什么情况下会为空值呢? - dipea
这不是一个很好的实现,它只是在某些情况下偶然能够工作。 - jcalz
1个回答

23

我认为我和你一样有同样的问题。简短回答是,我不认为这是一个很好的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 的快速信息显示方式不同,但这与先前的类型相同。



代码链接的沙盒


1
你的替代定义在原始挑战游乐场中不起作用,但我认为这是由于测试实现方式的某些特殊性造成的。(编辑-我在我的问题中添加了一个链接到挑战游乐场) - dipea
3
他们想要跳过函数吗?那么我会将其写为type DeepReadonly<T> = T extends Function ? T : { readonly [K in keyof T]: DeepReadonly<T[K]> };。当然,具体使用情况会推动定义;即使是内置的实用类型如Readonly<T>也会对函数进行奇怪的处理(例如Readonly<() => 22>会产生{}而不是() => 22),因此目前我认为在没有特定使用要求的情况下,“DeepReadonly”的“正确”定义仍然可能是我发布的那个定义。 - jcalz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接