12-类型里的逻辑运算:条件类型与 infer
课程
1
开篇:用正确的方式学习 TypeScript
已学完
学习时长: 7分24秒
2
工欲善其事:打造最舒适的 TypeScript 开发环境
学习时长: 19分14秒
3
进入类型的世界:理解原始类型与对象类型
学习时长: 37分40秒
4
掌握字面量类型与枚举,让你的类型再精确一些
学习时长: 24分53秒
5
函数与 Class 中的类型:详解函数重载与面向对象
学习时长: 50分40秒
6
探秘内置类型:any、unknown、never 与类型断言
学习时长: 34分58秒
7
类型编程好帮手:TypeScript 类型工具(上)
学习时长: 33分34秒
8
类型编程好帮手:TypeScript 类型工具(下)
学习时长: 40分52秒
9
类型编程基石:TypeScript 中无处不在的泛型
学习时长: 43分46秒
10
结构化类型系统:类型兼容性判断的幕后
学习时长: 20分48秒
11
类型系统层级:从 Top Type 到 Bottom Type
学习时长: 45分16秒
12
类型里的逻辑运算:条件类型与 infer
学习时长: 52分44秒
13
内置工具类型基础:别再妖魔化工具类型了!
学习时长: 44分47秒
14
反方向类型推导:用好上下文相关类型
学习时长: 18分37秒
15
函数类型:协变与逆变的比较
学习时长: 21分38秒
16
了解类型编程与类型体操的意义,找到平衡点
学习时长: 4分27秒
17
内置工具类型进阶:类型编程进阶
学习时长: 83分3秒
18
基础类型新成员:模板字符串类型入门
学习时长: 32分58秒
19
类型编程新范式:模板字符串工具类型进阶
学习时长: 69分27秒
20
工程层面的类型能力:类型声明、类型指令与命名空间
学习时长: 53分23秒
21
在 React 中愉快地使用 TypeScript:内置类型与泛型坑位
学习时长: 78分9秒
22
让 ESLint 来约束你的 TypeScript 代码:配置与规则集介绍
学习时长: 57分13秒
23
全链路 TypeScript 工具库,找到适合你的工具
学习时长: 25分13秒
24
说说 TypeScript 和 ECMAScript 之间那些事儿
学习时长: 29分14秒
25
装饰器与反射元数据:了解装饰器基本原理与应用
学习时长: 81分53秒
26
控制反转与依赖注入:基于装饰器的依赖注入实现
学习时长: 69分6秒
27
TSConfig 全解(上):构建相关配置
学习时长: 55分33秒
28
TSConfig 全解(下):检查相关、工程相关配置
学习时长: 76分19秒
29
基于 Prisma + NestJs 的 Node API :前置知识储备
学习时长: 44分50秒
30
基于 Prisma + NestJs 的 Node API :项目开发与基于 Heroku 部署
学习时长: 46分40秒
31
玩转 TypeScript AST:AST Checker 与 CodeMod
学习时长: 85分18秒
32
感谢相伴:是结束,也是开始
学习时长: 3分21秒
33
漫谈篇:面试中的 TypeScript
学习时长: 4分25秒
juejin_logo copyCreated with Sketch.

在完成类型层级一节的学习后,这一节学习条件类型对你来说已经没有什么困难了,因为你已经完全理解了它的判断逻辑!那我们直接开始这一节的学习吧!

本节代码见:Conditional Types

条件类型基础

条件类型的语法类似于我们平时常用的三元表达式,它的基本语法如下(伪代码):

ValueA === ValueB ? Result1 : Result2;
TypeA extends TypeB ? Result1 : Result2;

但需要注意的是,条件类型中使用 extends 判断类型的兼容性,而非判断类型的全等性。这是因为在类型层面中,对于能够进行赋值操作的两个变量,我们并不需要它们的类型完全相等,只需要具有兼容性,而两个完全相同的类型,其 extends 自然也是成立的。

条件类型绝大部分场景下会和泛型一起使用,我们知道,泛型参数的实际类型会在实际调用时才被填充(类型别名中显式传入,或者函数中隐式提取),而条件类型在这一基础上,可以基于填充后的泛型参数做进一步的类型操作,比如这个例子:

type LiteralType<T> = T extends string ? "string" : "other";

type Res1 = LiteralType<"linbudu">; // "string"
type Res2 = LiteralType<599>; // "other"

同三元表达式可以嵌套一样,条件类型中也常见多层嵌套,如:

export type LiteralType<T> = T extends string
	? "string"
	: T extends number
	? "number"
	: T extends boolean
	? "boolean"
	: T extends null
	? "null"
	: T extends undefined
	? "undefined"
	: never;

type Res1 = LiteralType<"linbudu">; // "string"
type Res2 = LiteralType<599>; // "number"
type Res3 = LiteralType<true>; // "boolean"

而在函数中,条件类型与泛型的搭配同样很常见。考考你,以下这个函数,我们应该如何标注它的返回值类型?

function universalAdd<T extends number | bigint | string>(x: T, y: T) {
    return x + (y as any);
}

当我们调用这个函数时,由于两个参数都引用了泛型参数 T ,因此泛型会被填充为一个联合类型:

universalAdd(599, 1); // T 填充为 599 | 1
universalAdd("linbudu", "599"); // T 填充为 linbudu | 599

那么此时的返回值类型就需要从这个字面量联合类型中推导回其原本的基础类型。在类型层级一节中,我们知道同一基础类型的字面量联合类型,其可以被认为是此基础类型的子类型,即 599 | 1 是 number 的子类型。

因此,我们可以使用嵌套的条件类型来进行字面量类型到基础类型地提取:

function universalAdd<T extends number | bigint | string>(
	x: T,
	y: T
): LiteralToPrimitive<T> {
	return x + (y as any);
}

export type LiteralToPrimitive<T> = T extends number
	? number
	: T extends bigint
	? bigint
	: T extends string
	? string
	: never;

universalAdd("linbudu", "599"); // string
universalAdd(599, 1); // number
universalAdd(10n, 10n); // bigint

条件类型还可以用来对更复杂的类型进行比较,比如函数类型:

type Func = (...args: any[]) => any;

type FunctionConditionType<T extends Func> = T extends (
  ...args: any[]
) => string
  ? 'A string return func!'
  : 'A non-string return func!';

//  "A string return func!"
type StringResult = FunctionConditionType<() => string>;
// 'A non-string return func!';
type NonStringResult1 = FunctionConditionType<() => boolean>;
// 'A non-string return func!';
type NonStringResult2 = FunctionConditionType<() => number>;

在这里,我们的条件类型用于判断两个函数类型是否具有兼容性,而条件中并不限制参数类型,仅比较二者的返回值类型。

与此同时,存在泛型约束和条件类型两个 extends 可能会让你感到疑惑,但它们产生作用的时机完全不同,泛型约束要求你传入符合结构的类型参数,相当于参数校验。而条件类型使用类型参数进行条件判断(就像 if else),相当于实际内部逻辑

我们上面讲到的这些条件类型,本质上就是在泛型基于调用填充类型信息的基础上,新增了基于类型信息的条件判断。看起来很不错,但你可能也发现了一个无法满足的场景:提取传入的类型信息。

infer 关键字

在上面的例子中,假如我们不再比较填充的函数类型是否是 (...args: any[]) => string 的子类型,而是要拿到其返回值类型呢?或者说,我们希望拿到填充的类型信息的一部分,而不是只是用它来做条件呢?

TypeScript 中支持通过 infer 关键字来在条件类型中提取类型的某一部分信息,比如上面我们要提取函数返回值类型的话,可以这么放:

type FunctionReturnType<T extends Func> = T extends (
  ...args: any[]
) => infer R
  ? R
  : never;

看起来是新朋友,其实还是老伙计。上面的代码其实表达了,当传入的类型参数满足 T extends (...args: any[] ) => infer R 这样一个结构(不用管 infer R,当它是 any 就行),返回 infer R 位置的值,即 R。否则,返回 never。

inferinference 的缩写,意为推断,如 infer RR 就表示 待推断的类型infer 只能在条件类型中使用,因为我们实际上仍然需要类型结构是一致的,比如上例中类型信息需要是一个函数类型结构,我们才能提取出它的返回值类型。如果连函数类型都不是,那我只会给你一个 never 。

这里的类型结构当然并不局限于函数类型结构,还可以是数组:

type Swap<T extends any[]> = T extends [infer A, infer B] ? [B, A] : T;

type SwapResult1 = Swap<[1, 2]>; // 符合元组结构,首尾元素替换[2, 1]
type SwapResult2 = Swap<[1, 2, 3]>; // 不符合结构,没有发生替换,仍是 [1, 2, 3]

由于我们声明的结构是一个仅有两个元素的元组,因此三个元素的元组就被认为是不符合类型结构了。但我们可以使用 rest 操作符来处理任意长度的情况:

// 提取首尾两个
type ExtractStartAndEnd<T extends any[]> = T extends [
  infer Start,
  ...any[],
  infer End
]
  ? [Start, End]
  : T;

// 调换首尾两个
type SwapStartAndEnd<T extends any[]> = T extends [
  infer Start,
  ...infer Left,
  infer End
]
  ? [End, ...Left, Start]
  : T;

// 调换开头两个
type SwapFirstTwo<T extends any[]> = T extends [
  infer Start1,
  infer Start2,
  ...infer Left
]
  ? [Start2, Start1, ...Left]
  : T;

是的,infer 甚至可以和 rest 操作符一样同时提取一组不定长的类型,而 ...any[] 的用法是否也让你直呼神奇?上面的输入输出仍然都是数组,而实际上我们完全可以进行结构层面的转换。比如从数组到联合类型:

type ArrayItemType<T> = T extends Array<infer ElementType> ? ElementType : never;

type ArrayItemTypeResult1 = ArrayItemType<[]>; // never
type ArrayItemTypeResult2 = ArrayItemType<string[]>; // string
type ArrayItemTypeResult3 = ArrayItemType<[string, number]>; // string | number

原理即是这里的 [string, number] 实际上等价于 (string | number)[]

除了数组,infer 结构也可以是接口:

// 提取对象的属性类型
type PropType<T, K extends keyof T> = T extends { [Key in K]: infer R }
  ? R
  : never;

type PropTypeResult1 = PropType<{ name: string }, 'name'>; // string
type PropTypeResult2 = PropType<{ name: string; age: number }, 'name' | 'age'>; // string | number

// 反转键名与键值
type ReverseKeyValue<T extends Record<string, unknown>> = T extends Record<infer K, infer V> ? Record<V & string, K> : never

type ReverseKeyValueResult1 = ReverseKeyValue<{ "key": "value" }>; // { "value": "key" }

在这里,为了体现 infer 作为类型工具的属性,我们结合了索引类型与映射类型,以及使用 & string 来确保属性名为 string 类型的小技巧。

为什么需要这个小技巧,如果不使用又会有什么问题呢?

// 类型“V”不满足约束“string | number | symbol”。
type ReverseKeyValue<T extends Record<string, string>> = T extends Record<
  infer K,
  infer V
>
  ? Record<V, K>
  : never;

明明约束已经声明了 V 的类型是 string,为什么还是报错了?

这是因为,泛型参数 V 的来源是从键值类型推导出来的,TypeScript 中这样对键值类型进行 infer 推导,将导致类型信息丢失,而不满足索引签名类型只允许 string | number | symbol 的要求。

还记得映射类型的判断条件吗?需要同时满足其两端的类型,我们使用 V & string 这一形式,就确保了最终符合条件的类型参数 V 一定会满足 string | never 这个类型,因此可以被视为合法的索引签名类型。

infer 结构还可以是 Promise 结构!

type PromiseValue<T> = T extends Promise<infer V> ? V : T;

type PromiseValueResult1 = PromiseValue<Promise<number>>; // number
type PromiseValueResult2 = PromiseValue<number>; // number,但并没有发生提取

就像条件类型可以嵌套一样,infer 关键字也经常被使用在嵌套的场景中,包括对类型结构深层信息地提取,以及对提取到类型信息的筛选等。比如上面的 PromiseValue,如果传入了一个嵌套的 Promise 类型就失效了:

type PromiseValueResult3 = PromiseValue<Promise<Promise<boolean>>>; // Promise<boolean>,只提取了一层

这种时候我们就需要进行嵌套地提取了:

type PromiseValue<T> = T extends Promise<infer V>
  ? V extends Promise<infer N>
    ? N
    : V
  : T;

当然,在这时应该使用递归来处理任意嵌套深度:

type PromiseValue<T> = T extends Promise<infer V> ? PromiseValue<V> : T;

条件类型在泛型的基础上支持了基于类型信息的动态条件判断,但无法直接消费填充类型信息,而 infer 关键字则为它补上了这一部分的能力,让我们可以进行更多奇妙的类型操作。TypeScript 内置的工具类型中还有一些基于 infer 关键字的应用,我们会在内置工具类型讲解一章中了解它们的具体实现。而我们上面了解的 rest infer(...infer Left),结合其他类型工具、递归 infer 等,都是日常比较常用的操作,这些例子应当能让你再一次意识到“类型编程”的真谛。

分布式条件类型

分布式条件类型听起来真的很高级,但这里和分布式和分布式服务并不是一回事。分布式条件类型(Distributive Conditional Type),也称条件类型的分布式特性,只不过是条件类型在满足一定情况下会执行的逻辑而已。我们来看一个例子:

type Condition<T> = T extends 1 | 2 | 3 ? T : never;

// 1 | 2 | 3
type Res1 = Condition<1 | 2 | 3 | 4 | 5>;

// never
type Res2 = 1 | 2 | 3 | 4 | 5 extends 1 | 2 | 3 ? 1 | 2 | 3 | 4 | 5 : never;

这个例子可能让你感觉充满了疑惑,某些地方似乎和我们学习的知识并不一样?先不说这两个理论上应该执行结果一致的类型别名,为什么在 Res1 中诡异地返回了一个联合类型?

仔细观察这两个类型别名的差异你会发现,唯一的差异就是在 Res1 中,进行判断的联合类型被作为泛型参数传入给另一个独立的类型别名,而 Res2 中直接对这两者进行判断。

记住第一个差异:是否通过泛型参数传入。我们再看一个例子:

type Naked<T> = T extends boolean ? "Y" : "N";
type Wrapped<T> = [T] extends [boolean] ? "Y" : "N";

// "N" | "Y"
type Res3 = Naked<number | boolean>;

// "N"
type Res4 = Wrapped<number | boolean>;

现在我们都是通过泛型参数传入了,但诡异的事情又发生了,为什么第一个还是个联合类型?第二个倒是好理解一些,元组的成员有可能是数字类型,显然不兼容于 [boolean]。再仔细观察这两个例子你会发现,它们唯一的差异是条件类型中的泛型参数是否被数组包裹了。

同时,你会发现在 Res3 的判断中,其联合类型的两个分支,恰好对应于分别使用 number 和 boolean 去作为条件类型判断时的结果。

把上面的线索理一下,其实我们就大致得到了条件类型分布式起作用的条件。首先,你的类型参数需要是一个联合类型 。其次,类型参数需要通过泛型参数的方式传入,而不能直接进行条件类型判断(如 Res2 中)。最后,条件类型中的泛型参数不能被包裹。

而条件类型分布式特性会产生的效果也很明显了,即将这个联合类型拆开来,每个分支分别进行一次条件类型判断,再将最后的结果合并起来(如 Naked 中)。如果再严谨一些,其实我们就得到了官方的解释:

对于属于裸类型参数的检查类型,条件类型会在实例化时期自动分发到联合类型上。Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation.

这里的自动分发,我们可以这么理解:

type Naked<T> = T extends boolean ? "Y" : "N";

// (number extends boolean ? "Y" : "N") | (boolean extends boolean ? "Y" : "N")
// "N" | "Y"
type Res3 = Naked<number | boolean>;

写成伪代码其实就是这样的:

const Res3 = [];

for(const input of [number, boolean]){
  if(input extends boolean){
    Res3.push("Y");
  } else {
    Res.push("N");
  }
}

而这里的裸类型参数,其实指的就是泛型参数是否完全裸露,我们上面使用数组包裹泛型参数只是其中一种方式,比如还可以这么做:

export type NoDistribute<T> = T & {};

type Wrapped<T> = NoDistribute<T> extends boolean ? "Y" : "N";

type Res1 = Wrapped<number | boolean>; // "N"
type Res2 = Wrapped<true | false>; // "Y"
type Res3 = Wrapped<true | false | 599>; // "N"

需要注意的是,我们并不是只会通过裸露泛型参数,来确保分布式特性能够发生。在某些情况下,我们也会需要包裹泛型参数来禁用掉分布式特性。最常见的场景也许还是联合类型的判断,即我们不希望进行联合类型成员的分布判断,而是希望直接判断这两个联合类型的兼容性判断,就像在最初的 Res2 中那样:

type CompareUnion<T, U> = [T] extends [U] ? true : false;

type CompareRes1 = CompareUnion<1 | 2, 1 | 2 | 3>; // true
type CompareRes2 = CompareUnion<1 | 2, 1>; // false

通过将参数与条件都包裹起来的方式,我们对联合类型的比较就变成了数组成员类型的比较,在此时就会严格遵守类型层级一文中联合类型的类型判断了(子集为其子类型)。

另外一种情况则是,当我们想判断一个类型是否为 never 时,也可以通过类似的手段:

type IsNever<T> = [T] extends [never] ? true : false;

type IsNeverRes1 = IsNever<never>; // true
type IsNeverRes2 = IsNever<"linbudu">; // false

这里的原因其实并不是因为分布式条件类型。我们此前在类型层级中了解过,当条件类型的判断参数为 any,会直接返回条件类型两个结果的联合类型。而在这里其实类似,当通过泛型传入的参数为 never,则会直接返回 never。

需要注意的是这里的 never 与 any 的情况并不完全相同,any 在直接作为判断参数时作为泛型参数时都会产生这一效果:

// 直接使用,返回联合类型
type Tmp1 = any extends string ? 1 : 2;  // 1 | 2

type Tmp2<T> = T extends string ? 1 : 2;
// 通过泛型参数传入,同样返回联合类型
type Tmp2Res = Tmp2<any>; // 1 | 2

// 如果判断条件是 any,那么仍然会进行判断
type Special1 = any extends any ? 1 : 2; // 1
type Special2<T> = T extends any ? 1 : 2;
type Special2Res = Special2<any>; // 1

而 never 仅在作为泛型参数时才会产生:

// 直接使用,仍然会进行判断
type Tmp3 = never extends string ? 1 : 2; // 1

type Tmp4<T> = T extends string ? 1 : 2;
// 通过泛型参数传入,会跳过判断
type Tmp4Res = Tmp4<never>; // never

// 如果判断条件是 never,还是仅在作为泛型参数时才跳过判断
type Special3 = never extends never ? 1 : 2; // 1
type Special4<T> = T extends never ? 1 : 2;
type Special4Res = Special4<never>; // never

这里的 any、never 两种情况都不会实际地执行条件类型,而在这里我们通过包裹的方式让它不再是一个孤零零的 never,也就能够去执行判断了。

之所以分布式条件类型要这么设计,我个人理解主要是为了处理联合类型这种情况。就像我们到现在为止的伪代码都一直使用数组来表达联合类型一样,在类型世界中联合类型就像是一个集合一样。通过使用分布式条件类型,我们能轻易地进行集合之间的运算,比如交集:

type Intersection<A, B> = A extends B ? A : never;

type IntersectionRes = Intersection<1 | 2 | 3, 2 | 3 | 4>; // 2 | 3

进一步的,当联合类型的组成是一个对象的属性名(keyof IObject),此时对这样的两个类型集合进行处理,得到属性名的交集,那我们就可以在此基础上获得两个对象类型结构的交集。除此以外,还有许多相对复杂的场景可以降维到类型集合,即联合类型的层面,然后我们就可以愉快地使用分布式条件类型进行各种处理了。关于类型层面的集合运算、对象结构集合运算,我们都会在小册的后续章节有详细的讲解。

总结与预告

在这一节,我们详细地解读了条件类型这一重要类型工具的使用方式、使用场景、分布式特性以及 infer 关键字。对于条件类型最核心的部分,即 extends 所代表的类型兼容性,由于在上一节我们已经了解了整个 TypeScript 类型系统的类型层级,因此在实际学习时其实基本没有什么压力,毕竟你已经参透了它最基础的运行规则。而对于 infer 关键字的使用,除了我们已经了解的,在函数结构、对象结构、数组结构等不同结构中的使用以外,请你不妨再试试它在更复杂场景下的使用,感受一下模式匹配的魅力。

在下一节,我们会开始探秘 TypeScript 内置的工具类型,看看它们是如何设计,以及又是用来解决什么问题的,并且思考如何让它们变得更完善。而在更后面,我们会了解更多类型系统的知识与更复杂的工具类型实现,欢迎与我一起深入类型编程的世界。

扩展阅读:IsAny 与 IsUnknown

上面我们通过比较 hack 的手段得到了 IsNever,那你一定会想是否能实现 IsAny 与 IsUnknown ?当然可以,只不过它们的实现稍微复杂一些,并且并不完全依赖分布式条件类型。

首先是 IsAny,上面已经提到我们并不能通过 any extends Type 这样的形式来判断一个类型是否是 any 。而是要利用 any 的另一个特性:身化万千:

type IsAny<T> = 0 extends 1 & T ? true : false;

0 extends 1 必然是不成立的,而交叉类型 1 & T 也非常奇怪,它意味着同时符合字面量类型 1 和另一个类型 T 。在学习交叉类型时我们已经了解,对于 1 这样的字面量类型,只有传入其本身、对应的原始类型、包含其本身的联合类型,才能得到一个有意义的值,并且这个值一定只可能是它本身:

type Tmp1 = 1 & (0 | 1); // 1
type Tmp2 = 1 & number; // 1
type Tmp3 = 1 & 1; // 1

这是因为交叉类型就像短板效应一样,其最终计算的类型是由最短的那根木板,也就是最精确的那个类型决定的。这样看,无论如何 0 extends 1 都不会成立。

但作为代表任意类型的 any ,它的存在就像是开天辟地的基本规则一样,如果交叉类型的其中一个成员是 any,那短板效应就失效了,此时最终类型必然是 any 。

type Tmp4 = 1 & any; // any

而对于 unknown 并不能享受到这个待遇,因为它并不是“身化万千”的:

type Tmp5 = 1 & unknown; // 1

因此,我们并不能用这个方式来写 IsUnknown。其实现过程要更复杂一些,我们需要过滤掉其他全部的类型来只剩下 unknown 。这里直接看实现:

type IsUnknown<T> = IsNever<T> extends false
  ? T extends unknown
    ? unknown extends T
      ? IsAny<T> extends false
        ? true
        : false
      : false
    : false
  : false;

首先过滤掉 never 类型,然后对于 T extends unknownunknown extends T,只有 any 和 unknown 类型能够同时符合(还记得我们在类型层级一节进行的尝试吗?),如果再过滤掉 any,那肯定就只剩下 unknown 类型啦。

更新:感谢评论区 红花绿叶肉夹馍 同学的指出,这里的 IsUnknown 类型其实可以使用更简单的方式实现。利用 unknown extends T 时仅有 T 为 any 或 unknown 时成立这一点,我们可以直接将类型收窄到 any 与 unknown,然后在去掉 any 类型时,我们仍然可以利用上面的身化万千特性:

type IsUnknown<T> = unknown extends T
  ? IsAny<T> extends true
    ? false
    : true
  : false;
留言
Ctrl + Enter
全部评论(58)
小喵呜的头像
删除
嚎哭深渊典狱长
打卡 脑袋有点转不过来了已经
点赞
回复
愤怒的小貔貅的头像
删除
还记得映射类型的判断条件吗?需要同时满足其两端的类型,我们使用 V & string 这一形式,就确保了最终符合条件的类型参数 V 一定会满足 string | never 这个类型,因此可以被视为合法的索引签名类型。

这里的映射类型应该也是写错了, 应该是交叉类型
点赞
回复
沁浒丶的头像
删除
搬砖🧱码农
rest infer应该是...infer T,而不是...any[]吧?
点赞
1
删除
(作者)
FIXED
点赞
回复
我必须马上跑路的头像
删除
这一章感觉好抽象,先跳过了,后面再来看[皱眉]
2
回复
qiao22的头像
删除
为什么要写这么多三元表达式,很难理解的
1
回复
樱桃小锤子的头像
删除
前端 @ 一只转行的咸鱼
滴滴滴
点赞
回复
Yancy昀的头像
删除
这篇真的看不懂啊,存档,之后再来[流泪]
3
回复
没有头罩的头盔的头像
删除
前端开发
export type NoDistribute<T> = T & {};为什么需要&{}
点赞
2
删除
因为裸类型会触发分发,从而返回联合类型, 数组元组对象Promise则不会触发分发
1
回复
删除
[惊喜]感谢 看了评论一下子明白了~
因为裸类型会触发分发,从而返回联合类型, 数组元组对象Promise则不会触发分发
点赞
回复
首席CV工程师的头像
删除
前端开发 @ 国家摸鱼办
越来越难啃~
点赞
回复
FuncJin的头像
删除
大佬问一下,这个应该如何让bar为1,而不是number类型呀
interface C {
name: string,
age: number
}
type Q = Array<C>
const tuple = <T extends Q>(...args: T) => args
const a = tuple({ name: '鲨鱼辣椒', age: 1 })
const bar = a[0].age
收起
点赞
7
删除
(作者)
const a = tuple({ name: '鲨鱼辣椒', age: 1 } as const)
点赞
回复
删除
用const,如果对象很多,那就得不停的as const呀
const a = tuple({ name: '鲨鱼辣椒', age: 1 } as const)
点赞
回复
删除
如果是as const,那tuple就没用了
const a = tuple({ name: '鲨鱼辣椒', age: 1 } as const)
点赞
回复
删除
(作者)
回复
对于对象值,如果不常量断言,类型推导只能推导到值类型,不能推导到字面量类型
用const,如果对象很多,那就得不停的as const呀
点赞
回复
删除
extends infer也不可以吗
对于对象值,如果不常量断言,类型推导只能推导到值类型,不能推导到字面量类型
点赞
回复
删除
(作者)
回复
infer 也不会推导到字面量类型,而且如果有很多对象的话,为啥还会需要这种严格的字面量类型推断呢
extends infer也不可以吗
点赞
回复
删除
确实,不如用as const方便。有时候对象键会被推为string类型,而string类型往往不能用在类型收缩(as const)的键上
infer 也不会推导到字面量类型,而且如果有很多对象的话,为啥还会需要这种严格的字面量类型推断呢
点赞
回复
涛涛_江的头像
删除
打卡
点赞
回复
前端摸鱼儿的头像
删除
```把上面的线索理一下,其实我们就大致得到了条件类型分布式起作用的条件。首先,你的类型参数需要是一个联合类型 。其次,类型参数需要通过泛型参数的方式传入,而不能直接在外部进行判断(如 Res2 中)。最后,条件类型中的泛型参数不能被包裹。```

而不能直接在(内)部进行判断(如 Res2 中) 是内部吧
点赞
1
删除
(作者)
FIXED
点赞
回复
小路就是我的头像
删除
不会开发工程师 @ 韩创科技
这里写错了吧? type Wrapped<T> = NoDistribute<T> extends [boolean] ? "Y" : "N"; 是不是应该是:type Wrapped<T> = NoDistribute<T> extends boolean ? "Y" : "N";
1
1
删除
(作者)
已回复,见原评论
点赞
回复
千味的头像
删除
前端码农
不明白为啥【最终符合条件的类型参数 V 一定会满足 string | never】,难道不应该理解为infer 推导出来的类型是unkonw吗,所以最后需要与 string 交叉。
点赞
1
删除
(作者)
一个是结果一个是原因,直接放结果更好理解吧,当然怎么理解不重要,能理解就好了
点赞
回复
沉江河的头像
删除
端水大师 @ 松花江畔
越往后的章节 啃的时间越长[泣不成声]
点赞
回复
codelin的头像
删除
export type NoDistribute<T> = T & {};
type Wrapped<T> = NoDistribute<T> extends [boolean] ? "Y" : "N";
------------------------------
渡哥,想问问此处使用 T & {}要怎么理解,我发现传进什么值,返回的都是N
type Q = Wrapped<true>//N
type Q2 = Wrapped<boolean>//N
1
6
删除
(作者)
需要对比分布式条件类型理解,这里阻止了分布式的产生,就只能比较 true & {} 和 [boolean] 这两个类型,必然是不成立的
点赞
回复
删除
文中是不是写错了,是不是应该写成:type Wrapped<T> = NoDistribute<T> extends boolean ? "Y" : "N";
需要对比分布式条件类型理解,这里阻止了分布式的产生,就只能比较 true & {} 和 [boolean] 这两个类型,必然是不成立的
点赞
回复
删除
没错呀,不可能一个加了数组另一个不加吧,那样的话类型比较就没有意义了,你可以看下面的联合类型包裹后比较也是一样的
文中是不是写错了,是不是应该写成:type Wrapped<T> = NoDistribute<T> extends boolean ? "Y" : "N";
点赞
回复
删除
条件判断的左侧不是数组啊,是T & {}
没错呀,不可能一个加了数组另一个不加吧,那样的话类型比较就没有意义了,你可以看下面的联合类型包裹后比较也是一样的
点赞
回复
删除
哦哦是第二个例子[流泪],我还以为是第一个,这里确实应该换成裸露的boolean,感谢
条件判断的左侧不是数组啊,是T & {}
1
回复
删除
已经修正并新增调用示例
条件判断的左侧不是数组啊,是T & {}
1
回复
苏州前端_张不皱的头像
删除
前端开发 @ garena
infer推导出来的类型可以理解为any类型,希望作者可以加下是否可以为空的讲解。例如type ExtractFirstAndEnd<T extends unknown[]> = T extends [infer A, ...infer B, infer C] ? [A,C] : T

推导出来的类型B为unknown[],它可以是空数组,也就是说length为2即可满足上述条件类型;
还有模版字符串那里
type test<T extends string> = T extends `${infer A}${infer B}` ? true : false
通过测试发现空字符串可以作为一个类型,'a'就可以满足上述条件类型。
收起
点赞
2
删除
inter 推导出来的类型应该理解为unknow类型吧,这样才符合后面 & string 交叉得到 string,如果是any 交叉 string 的话是得到any
2
回复
删除
嗯,对的,any交叉万物都是any
inter 推导出来的类型应该理解为unknow类型吧,这样才符合后面 & string 交叉得到 string,如果是any 交叉 string 的话是得到any
点赞
回复
苏州前端_张不皱的头像
删除
前端开发 @ garena
isAny还可以利用身化万千实现:
type _isAny<T> = T extends boolean ? true : false;
type isAny<T> = _isAny<T> extends boolean
? boolean extends _isAny<T>
? true
: false
: false;
收起
点赞
回复
Revol_C的头像
删除
前端开发
这里的泛型参数传入的应该不是“条件类型”,而是“联合类型”吧
image
点赞
1
删除
(作者)
已更新~
点赞
回复
我睡前一定喝牛奶的头像
删除
社会闲散青年 @ 无业
【universalAdd(599, 1); // T 填充为 599 | 1 】如果改成【universalAdd(599, "1");】这个时候会报错,推断出来的是599,理解成泛型的自动推断是根据第一个参数进行推断,如果其他参数也属于第一个参数的父类型(string)的子类型(如1)就联合,如果不是他的子类型(如字符串1),就以第一个参数推断出来的类型作为泛型参数,这样理解对吗...大佬,(这本小册太棒了...
点赞
2
删除
(作者)
是的,我理解对泛型的推导是从左向右进行,universalAdd(599, 1) 会在第一个参数推导出 599 类型,然后在第二个参数进一步扩充这个类型,尝试找到一个同时满足这两个参数类型的类型(尝试在 number 下扩展,最终扩展为 599 | 1),universalAdd(599, "1") 这种情况就是无法进行合法扩展了
1
回复
删除
要是把这段说明补充在上面就更棒了
是的,我理解对泛型的推导是从左向右进行,universalAdd(599, 1) 会在第一个参数推导出 599 类型,然后在第二个参数进一步扩充这个类型,尝试找到一个同时满足这两个参数类型的类型(尝试在 number 下扩展,最终扩展为 599 | 1),universalAdd(599, "1") 这种情况就是无法进行合法扩展了
点赞
回复
我睡前一定喝牛奶的头像
删除
社会闲散青年 @ 无业
【因为泛型参数 V 的来源是从 unknown 这个类型推导出来的,它将被作为新的类型的索引签名类型】,这里我测试将unknown改成any也是会报错的,会不会泛型V没有从那里的类型推导,我的理解是V & string只可能生成never和string两种类型所以符合了键值判断...不知道对不对大佬
点赞
1
删除
(作者)
[嘿哈]是应该这么理解,已更新~
点赞
回复
用户4580432221603的头像
删除
这里的“映射类型”是不是应该笔误了?应该是交叉类型?上文一直提的是交叉类型,怎么突然提到映射类型了?
type Tmp1 = 1 & (0 | 1); //
1
type Tmp2 = 1 & number; // 1
type Tmp3 = 1 & 1; // 1
收起
image
点赞
1
删除
(作者)
感谢,已修改~
点赞
回复
rubin本尊的头像
删除
记录一下

通过将参数与条件都包裹起来的方式,我们对联合类型的比较就变成了数组成员类型的比较,在此时就会严格遵守类型层级一文中联合类型的类型判断了(子集为其子类型)。
点赞
回复
红花绿叶肉夹馍的头像
删除
前端 @ 电信
IsUnknown 为什么要先判定IsNever 和T extends unknown 呢?直接从 unknown extends T 开始做逻辑判断不也是可行的么,unknown作为Top Type , unknown extends T 后不是就只有any和 unknown了嘛
4
1
删除
(作者)
确实可以更简单,IsAny 都可以不用了,我会更新一下的~
1
回复
你看起来好像很好吃的头像
删除
FE @ 快手
尝试了一下作者说的获取两个对象的交集,不知道对不对
type Intersection<A, B> = A extends B ? A : never;
type IntersectionObject<A, B> = {[K in Intersection<keyof A, keyof B>]: A[K]};
type IntersectionObjectRes = IntersectionObject<{"age": number; "other": string}, {"name": string; "age": number}>; // { "age": number }
点赞
2
删除
貌似不行,age其中一个是string的话就不行了
点赞
回复
删除
改为这种不知道行不行
image
貌似不行,age其中一个是string的话就不行了
点赞
回复
zhedream的头像
删除
全栈工程师 @ @zhedream
mark, infer 好像个占位符.
点赞
回复
拍照的小盆爸的头像
删除
前端 @ 天房科技
type Swap<T extends any[]> = T extends [infer T, infer U] ? [U, T] : T;

刚又看了下 数组内的T 就是 infer T,因为inder T是定义在数组内的,最右边的T因为不是在数组里面的,所以就是 入参里面定义的T
image
点赞
回复
拍照的小盆爸的头像
删除
前端 @ 天房科技
type Swap<T extends any[]> = T extends [infer T, infer U] ? [U, T] : T;
这里泛型用的T的变量,这个数组里面的 infer T,就和外面变量重名了,容易误导初学者。
我理解 后面的infer T 是入参泛型个T数组的第一个参数。
不知道作者看看,我理解的对不对,入参是T,后面返回的变量应该起个别的名字
点赞
1
删除
(作者)
感谢,已更新
点赞
回复
The action has been successful