百科狗-知识改变命运!
--

条件类型 - TypeScript 创建类型

是丫丫呀1年前 (2023-11-21)阅读数 24#技术干货
文章标签类型

条件类型

很多时候,我们需要基于输入的值来决定输出的值,同样我们也需要基于输入的值的类型来决定输出的值的类型。条件类型(Conditional types)就是用来帮助我们描述输入类型和输出类型之间的关系。

interface Animal {
  live(): void;
}

interface Dog extends Animal {
  woof(): void;
}
 
type Example1 = Dog extends Animal ? number : string;     
// type Example1 = number
 
type Example2 = RegExp extends Animal ? number : string;     
// type Example2 = string

条件类型的写法有点类似于 JavaScript 中的条件表达式(condition ? trueExpression : falseExpression):

SomeType extends OtherType ? TrueType : FalseType;

单从这个例子,可能看不出条件类型有什么用,但当搭配泛型使用的时候就很有用了,让我们拿下面的createLabel函数为例:

interface IdLabel {
  id: number /* some fields */;
}
interface NameLabel {
  name: string /* other fields */;
}
 
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
  throw "unimplemented";
}

条件类型 - TypeScript 创建类型

这里使用了函数重载,描述了createLabel是如何基于输入值的类型不同而做出不同的决策,返回不同的类型。注意这样一些事情:

  1. 如果一个库不得不在一遍又一遍的遍历 API 后做出相同的选择,它会变得非常笨重。
  2. 我们不得不创建三个重载,一个是为了处理明确知道的类型,我们为每一种类型都写了一个重载(这里一个是string,一个是number),一个则是为了通用情况(接收一个string | number)。而如果增加一种新的类型,重载的数量将呈指数增加。

其实我们完全可以用把逻辑写在条件类型中:

type NameOrId = T extends number
? IdLabel
: NameLabel;

使用这个条件类型,我们可以简化掉函数重载:

function createLabel(idOrName: T): NameOrId {
    throw "unimplemented";
}

let a = createLabel("typescript");
// let a: NameLabel

let b = createLabel(2.8);
// let b: IdLabel

let c = createLabel(Math.random() ? "hello" : 42);
// let c: NameLabel | IdLabel


条件类型约束

通常,使用条件类型会为我们提供一些新的信息。正如使用类型保护(type guards)可以收窄类型(narrowing)为我们提供一个更加具体的类型,条件类型的true分支也会进一步约束泛型,举个例子:

type MessageOf = T["message"];
// Type '"message"' cannot be used to index type 'T'.

TypeScript 报错是因为T不知道有一个名为message的属性。我们可以约束T,这样 TypeScript 就不会再报错:

type MessageOf = T["message"];
interface Email {
    message: string;
}

type EmailMessageContents = MessageOf;
// type EmailMessageContents = string

但是,如果我们想要MessgeOf可以传入任何类型,但是当传入的值没有message属性的时候,则返回默认类型比如never呢?

我们可以把约束移出来,然后使用一个条件类型:

type MessageOf = T extends { message: unknown } ? T["message"] : never;

interface Email {
    message: string;
}

interface Dog {
    bark(): void;
}

type EmailMessageContents = MessageOf;           
// type EmailMessageContents = string

type DogMessageContents = MessageOf;          
// type DogMessageContents = never

true分支里,TypeScript 会知道T有一个message属性。

再举一个例子,我们写一个Flatten类型,用于获取数组元素的类型,当传入的不是数组,则直接返回传入的类型:

type Flatten = T extends any[] ? T[number] : T;

// Extracts out the element type.
type Str = Flatten;  
// type Str = string

// Leaves the type alone.
type Num = Flatten;  
// type Num = number

注意这里使用了索引访问类型(opens new window)里的number索引,用于获取数组元素的类型。


在条件类型里推断

条件类型提供了infer关键词,可以从正在比较的类型中推断类型,然后在true分支里引用该推断结果。借助infer,我们修改下Flatten的实现,不再借助索引访问类型“手动”的获取出来:

type Flatten = Type extends Array ? Item : Type;

这里我们使用了infer关键字声明了一个新的类型变量Item,而不是像之前在true分支里明确的写出如何获取T的元素类型,这可以解放我们,让我们不用再苦心思考如何从我们感兴趣的类型结构中挖出需要的类型结构。

我们也可以使用infer关键字写一些有用的类型帮助别名(helper type aliases)。举个例子,我们可以获取一个函数返回的类型:

type GetReturnType = Type extends (...args: never[]) => infer Return
? Return
: never;

type Num = GetReturnType number>;
// type Num = number

type Str = GetReturnType string>;
// type Str = string

type Bools = GetReturnType boolean[]>;   
// type Bools = boolean[]

当从多重调用签名(就比如重载函数)中推断类型的时候,会按照最后的签名进行推断,因为一般这个签名是用来处理所有情况的签名。

declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;

type T1 = ReturnType;                     
// type T1 = string | number


分配条件类型

当传入的类型参数为联合类型时,他们会被分配类型。举个例子:

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

如果我们在ToArray传入一个联合类型,这个条件类型会被应用到联合类型的每个成员:

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

type StrArrOrNumArr = ToArray;        
// type StrArrOrNumArr = string[] | number[]

让我们分析下StrArrOrNumArr里发生了什么,这是我们传入的类型:

string | number;

接下来遍历联合类型里的成员,相当于:

ToArray | ToArray;

所以最后的结果是:

string[] | number[];

通常这是我们期望的行为,如果你要避免这种行为,你可以用方括号包裹extends关键字的每一部分。

type ToArrayNonDist = [Type] extends [any] ? Type[] : never;

// 'StrArrOrNumArr' is no longer a union.
type StrArrOrNumArr = ToArrayNonDist;
// type StrArrOrNumArr = (string | number)[]

鹏仔微信 15129739599 鹏仔QQ344225443 鹏仔前端 pjxi.com 共享博客 sharedbk.com

免责声明:我们致力于保护作者版权,注重分享,当前被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理!邮箱:344225443@qq.com)

图片声明:本站部分配图来自网络。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

内容声明:本文中引用的各种信息及资料(包括但不限于文字、数据、图表及超链接等)均来源于该信息及资料的相关主体(包括但不限于公司、媒体、协会等机构)的官方网站或公开发表的信息。部分内容参考包括:(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供参考使用,不准确地方联系删除处理!本站为非盈利性质站点,本着为中国教育事业出一份力,发布内容不收取任何费用也不接任何广告!)