JavaScript内核与高级编程之:`TypeScript` 的 `Conditional Types`:其在泛型编程中的条件判断。

各位靓仔靓女们,很高兴今天能和大家聊聊 TypeScript 里的一个相当酷炫的玩意儿——条件类型(Conditional Types)。这玩意儿,听起来高大上,其实就是让 TypeScript 的类型系统也能玩条件判断,就像 if...else 一样。有了它,我们的类型定义就能更加灵活,更加智能,简直是泛型编程的福音!

开场白:TypeScript 类型系统的一点抱怨

说实话,在没遇到条件类型之前,我对 TypeScript 的类型系统是又爱又恨。爱的是它能帮我揪出很多低级错误,恨的是有时候它太死板了,稍微复杂一点的逻辑就搞不定。

比如,我想定义一个函数,如果传入的是字符串,就返回字符串的长度,如果传入的是数字,就返回数字的平方。这在 JavaScript 里简直是小菜一碟,但在 TypeScript 里,如果没有条件类型,就得用各种类型断言或者函数重载,代码一下子就变得臃肿不堪。

// 传统的做法,略显笨拙
function processData(input: string): number;
function processData(input: number): number;
function processData(input: string | number): number {
  if (typeof input === 'string') {
    return input.length;
  } else {
    return input * input;
  }
}

但是有了条件类型,一切就变得不一样了!

什么是条件类型?

条件类型允许我们根据一个类型表达式的结果来选择不同的类型。它的语法形式长这样:

T extends U ? X : Y

简单解释一下:

  • TU 是类型。
  • extends 关键字表示类型 T 是否可以赋值给类型 U,也就是 T 是不是 U 的子类型。
  • ?: 就像 JavaScript 里的三元运算符,如果 T extends U 的结果是 true,那么类型就是 X,否则就是 Y

是不是有点像 if...else?没错,它就是在类型层面上的 if...else

条件类型的基本用法:类型推断的利器

让我们用一个简单的例子来演示一下条件类型:

type IsString<T> = T extends string ? true : false;

type StringCheck = IsString<"hello">; // true
type NumberCheck = IsString<123>; // false

在这个例子中,我们定义了一个名为 IsString 的条件类型,它接受一个类型参数 T。如果 T 是字符串类型,那么 IsString<T> 的类型就是 true,否则就是 false

这看起来好像没什么大不了的,但是,条件类型真正的威力在于它可以和类型推断结合使用。

infer 关键字:类型推断的魔法棒

infer 关键字是条件类型里的一个大杀器。它可以让 TypeScript 在条件判断的过程中自动推断出某个类型。它的语法形式是这样的:

T extends infer R ? X : Y

在这个表达式中,如果 T 可以赋值给某个类型,那么 TypeScript 就会把这个类型推断出来,并且用 R 来表示。然后在 X 类型里,我们就可以使用 R 了。

举个例子,假设我们想从一个函数类型中提取出它的返回值类型。有了 infer 关键字,这简直是易如反掌:

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

type MyFunc = (a: number, b: string) => boolean;
type MyFuncReturnType = ReturnType<MyFunc>; // boolean

在这个例子中,我们定义了一个名为 ReturnType 的条件类型,它接受一个函数类型 T。如果 T 是一个函数类型,那么 TypeScript 就会自动推断出它的返回值类型,并且用 R 来表示。然后,ReturnType<T> 的类型就是 R

条件类型的高级用法:类型体操的乐园

条件类型不仅可以用来做简单的类型判断,还可以用来做一些非常复杂的类型操作,比如:

  • 提取联合类型中的特定类型
  • 过滤掉联合类型中的 null 和 undefined
  • 实现类型级别的递归

这些操作,我们通常称之为 "类型体操"。听起来很吓人,但其实只要掌握了条件类型和 infer 关键字,你也能轻松玩转类型体操。

实例解析:类型体操实战

咱们来玩几个稍微复杂点的例子,让大家感受一下条件类型的魅力。

1. 提取联合类型中的特定类型

假设我们有一个联合类型 type MyUnion = string | number | boolean,我们想从中提取出所有的字符串类型。怎么做呢?

type StringInUnion<T> = T extends string ? T : never;
type ExtractString<T> = T extends any ? StringInUnion<T> : never;

type MyUnion = string | number | boolean;
type StringType = ExtractString<MyUnion>; // string

在这个例子中,我们定义了两个条件类型:

  • StringInUnion<T>:如果 T 是字符串类型,那么返回 T,否则返回 nevernever 表示永远不会出现的类型,可以用来从联合类型中排除某个类型。
  • ExtractString<T>:这个类型利用了分布式条件类型。当 T 是一个联合类型时,T extends any ? StringInUnion<T> : never 相当于对联合类型中的每个成员都执行 StringInUnion<T> 操作,然后将结果合并成一个新的联合类型。

2. 过滤掉联合类型中的 nullundefined

假设我们有一个联合类型 type NullableType = string | null | undefined,我们想从中过滤掉 nullundefined。怎么做呢?

type NonNullable<T> = T extends null | undefined ? never : T;

type NullableType = string | null | undefined;
type NonNullableStringType = NonNullable<NullableType>; // string

在这个例子中,我们定义了一个名为 NonNullable 的条件类型。如果 Tnullundefined,那么返回 never,否则返回 T

3. 实现类型级别的递归(困难警告!)

这个例子比较复杂,需要对条件类型和类型推断有比较深入的理解。

假设我们想定义一个类型,它可以将一个嵌套的数组类型展平。比如,Flatten<string[][]> 的类型应该是 stringFlatten<number[][][]> 的类型应该是 number

type Flatten<T> = T extends (infer U)[] ? Flatten<U> : T;

type NestedArray = string[][][];
type FlattenedArray = Flatten<NestedArray>; // string

这个例子中,我们定义了一个名为 Flatten 的条件类型。如果 T 是一个数组类型,那么 TypeScript 就会自动推断出数组元素的类型,并且用 U 来表示。然后,Flatten<T> 的类型就是 Flatten<U>,也就是对数组元素的类型递归调用 Flatten。如果 T 不是一个数组类型,那么 Flatten<T> 的类型就是 T

条件类型与泛型编程

条件类型是泛型编程中不可或缺的一部分。它可以让我们根据类型参数的不同来选择不同的类型,从而实现更加灵活和可复用的代码。

例如,我们可以定义一个通用的类型转换函数,它可以将不同的类型转换为字符串类型:

type ToString<T> = T extends string ? T : T extends number ? string : T extends boolean ? string : string;

function toString<T>(value: T): ToString<T> {
  if (typeof value === 'string') {
    return value as ToString<T>;
  } else if (typeof value === 'number') {
    return value.toString() as ToString<T>;
  } else if (typeof value === 'boolean') {
    return value.toString() as ToString<T>;
  } else {
    return String(value) as ToString<T>;
  }
}

const str: string = toString("hello"); // string
const numStr: string = toString(123); // string
const boolStr: string = toString(true); // string

在这个例子中,我们定义了一个名为 ToString 的条件类型,它可以根据类型参数 T 的不同来选择不同的字符串类型。然后,我们定义了一个名为 toString 的泛型函数,它可以接受任何类型的参数,并且返回对应的字符串类型。

一些需要注意的点

在使用条件类型的时候,有一些需要注意的点:

  • 分布式条件类型:当 T 是一个联合类型时,T extends U ? X : Y 相当于对联合类型中的每个成员都执行 T extends U ? X : Y 操作,然后将结果合并成一个新的联合类型。
  • never 类型never 类型表示永远不会出现的类型,可以用来从联合类型中排除某个类型。
  • 类型推断的局限性:有时候,TypeScript 的类型推断可能无法完全满足我们的需求,我们需要手动指定类型。

总结

条件类型是 TypeScript 类型系统中的一个非常强大的工具。它可以让我们根据类型表达式的结果来选择不同的类型,从而实现更加灵活和智能的类型定义。掌握条件类型,你就能轻松玩转类型体操,写出更加健壮和可维护的 TypeScript 代码。

条件类型,extendsinfer 三者关系总结表

特性 extends infer 条件类型 ( T extends U ? X : Y )
功能 类型约束,判断类型关系 类型推断,捕获类型信息 类型选择,基于条件选择不同类型
用法 T extends U:判断 T 是否可以赋值给 U T extends infer R ? ...:从 T 中推断类型 R T extends U ? X : Y:如果 T 可以赋值给 U,则类型为 X,否则为 Y
位置 类型约束,条件类型 条件类型 主要组成部分
作用对象 类型,通常用于泛型约束 类型,通常用于函数类型或数组类型等 类型,决定最终类型结果
示例 type MyType<T extends string> = ... type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any type IsString<T> = T extends string ? true : false
关系 条件类型依赖 extends 进行判断 条件类型可以使用 infer 进行类型推断 extendsinfer 共同构建了条件类型的强大功能

结束语

好了,今天的讲座就到这里。希望大家通过今天的学习,能够对条件类型有一个更深入的了解。下次再遇到复杂的类型问题,不妨试试条件类型,说不定会有意想不到的惊喜哦! 记得多练习,熟能生巧!溜了溜了!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注