TypeScript `extends` 条件类型语句仅在使用泛型表达时才有效?

TypeScript `extends` conditional type statement only works if expressed using Generics?

我正在努力更好地理解 TypeScript 中的 extends 关键字及其潜在应用。

我遇到的一件事是两个内置实用程序,ExtractExclude,它们同时利用了 extends 和条件输入。

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;
/**
 * Extract from T those types that are assignable to U
 */
type Extract<T, U> = T extends U ? T : never;

我一直在努力更好地理解这种“缩小范围”,或者更好地说“子集过滤”是如何工作的,并且尝试创建我自己的实现只是为了看看它的实际效果,并且遇到了这个非常奇怪的问题行为:

Link to the Playground Example:

type ValueSet = string | 'lol' | 0 | {a: 1} | number[] | 643;


type CustomExclude<T, U> = T extends U ? T : never;

// this works:
// type Result1 = 0 | 643
type Result1 =  CustomExclude<ValueSet, number>; 


// but this doesn't?
// type Result2 = never
type Result2 = ValueSet extends number ? ValueSet : never;

为什么会这样?

我希望两个实例都return类型的正确子集,但条件类型只有在通过泛型表达时才有效。

谁能给我解释一下这背后的逻辑?

第二段代码正在执行一次检查,以查看整个类型是否从 number 扩展而来。如果是,它 returns 整个类型,否则它 returns never。具有泛型的版本将逐步遍历联合中的所有单个类型(首先是 string,然后是 "lol",然后是 0 等)并单独评估它们。然后你会得到一个存活下来的个体类型的联合。

可以从第二个示例中得到一个非 never 值,但前提是每个可能的值都是数字。例如:

type Example = 1 | 3 | 5;
type Example2 = Example extends number ? Example : never;
//  Example2 is 1 | 3 | 5

请参阅distributive-conditional-types:

When conditional types act on a generic type, they become distributive when given a union type. For example, take the following:

type ToArray<Type> = Type extends any ? Type[] : never;

If we plug a union type into ToArray, then the conditional type will be applied to each member of that union.

type ToArray<Type> = Type extends any ? Type[] : never;
 
type StrArrOrNumArr = ToArray<string | number>; // string[] | number[]

因此,如果您将 extends 与泛型一起使用,整个条件类型将应用于联合中的每个元素。

如果您将 extends 用于非通用类型,就像您在第二个示例中使用的那样,条件类型将应用于整个类型。

您甚至可以在第一个示例中关闭分布性。只需将您的泛型包装在方括号中即可:

type ValueSet = string | 'lol' | 0 | {a: 1} | number[] | 643;


type CustomExclude<T, U> = [T] extends [U] ? T : never;

// never
type Result1 =  CustomExclude<ValueSet, number>; 


包裹在方括号中的泛型被视为非泛型类型,就像您的第一个示例一样。

在实践中,这个模式非常有用。通常使用 T extends any 只是为了打开分配。

假设您有一些对象类型。您想要获取所有密钥并对它们应用一些修改器。换句话说,映射它们。考虑这个例子:

type Foo = {
    name: string;
    age: number
}

// non verbose approach, distributivity

type ChangeKey<T> = keyof T extends string ? `${keyof T}-updated` : never
type Result = ChangeKey<Foo>


// middle verbose approach
type ChangeKey1<T> = {
    [Prop in keyof T]: Prop extends string ? `${Prop}-updated` : never
}[keyof T]

type Result1 = ChangeKey1<Foo>

// verbose approach
type ChangeKey2<T extends Record<string, unknown>> = keyof {
    [Prop in keyof T as Prop extends string ? `${Prop}-updated` : never]: never
}

type Result2 = ChangeKey2<Foo>

您可能已经注意到,ChangeKey 比其他人更优雅。