MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript 条件类型的语法与实践

2023-09-156.3k 阅读

条件类型基础语法

在 TypeScript 中,条件类型允许我们根据类型关系进行类型选择。其基本语法为:T extends U ? X : Y。这类似于 JavaScript 中的三元表达式 condition ? ifTrue : ifFalse,只不过这里是基于类型进行判断。

例如,假设有如下代码:

type IsString<T> = T extends string ? true : false;
type StringCheck = IsString<string>; // true
type NumberCheck = IsString<number>; // false

在上述代码中,IsString 是一个条件类型,它接收一个类型参数 T。如果 Tstring 类型,就返回 true 类型;否则返回 false 类型。

条件类型中的类型推断

在条件类型 T extends U ? X : Y 中,当 T 满足 U 类型约束时,我们可以在 X 类型中进行类型推断。

type Unpacked<T> = T extends Array<infer U> ? U : T;
type StrArray = string[];
type UnpackedStr = Unpacked<StrArray>; // string
type Num = number;
type UnpackedNum = Unpacked<Num>; // number

Unpacked 类型中,infer U 表示在 T 满足 Array<U> 这种类型结构时,推断出 U 的类型。如果 T 不是数组类型,就直接返回 T 本身。

分布式条件类型

当条件类型作用于联合类型时,会产生分布式行为。

type ToString<T> = T extends string ? string : never;
type StrOrNum = string | number;
type Result = ToString<StrOrNum>; // string

这里 StrOrNumstring | number 联合类型,ToString 条件类型会将联合类型中的每个成员分别应用条件判断,最终结果是联合类型中满足条件的成员所对应的类型组成的联合类型。因为 string 满足 T extends string 条件,所以结果是 string

条件类型的嵌套

条件类型可以进行嵌套,以实现更复杂的类型逻辑。

type IsStringOrNumber<T> = T extends string
  ? true
  : T extends number
  ? true
   : false;
type Check1 = IsStringOrNumber<string>; // true
type Check2 = IsStringOrNumber<boolean>; // false

IsStringOrNumber 类型中,先判断 T 是否为 string 类型,如果不是再判断是否为 number 类型,最后返回相应的结果。

条件类型与映射类型结合

将条件类型与映射类型结合,可以对对象类型的属性进行更灵活的类型转换。

type Optionalize<T> = {
  [P in keyof T]?: T[P];
};
type Requiredize<T> = {
  [P in keyof T]-?: T[P];
};
type IsStringProp<T> = {
  [P in keyof T]: T[P] extends string ? true : false;
};
interface User {
  name: string;
  age: number;
}
type OptionalUser = Optionalize<User>;
type RequiredUser = Requiredize<OptionalUser>;
type StringProps = IsStringProp<User>;

Optionalize 类型中,通过映射类型将对象 T 的所有属性变为可选。Requiredize 则相反,将可选属性变为必选。IsStringProp 用于判断对象 T 中每个属性是否为 string 类型。

条件类型在函数重载中的应用

条件类型也可以在函数重载中发挥作用,帮助我们根据不同的输入类型返回不同的输出类型。

function identity<T>(arg: T): T;
function identity<T extends string>(arg: T): T & { length: number };
function identity<T>(arg: T) {
  return arg;
}
let result1 = identity(123); // number
let result2 = identity('abc'); // string & { length: number }

这里第一个函数声明是通用的 identity 函数,第二个函数声明是针对 Tstring 类型的重载。当传入 string 类型参数时,会调用第二个重载,返回的类型会带有 length 属性。

条件类型在 React 组件开发中的实践

在 React 组件开发中,条件类型可以帮助我们更好地定义组件的 props 类型。

import React from 'react';
type Props<T> = {
  data: T;
  render: (item: T) => React.ReactNode;
};
function GenericList<T>({ data, render }: Props<T>) {
  return (
    <ul>
      {data.map((item) => (
        <li key={Math.random().toString(36).substr(2, 9)}>{render(item)}</li>
      ))}
    </ul>
  );
}
interface User {
  name: string;
  age: number;
}
function UserListItem({ name, age }: User) {
  return (
    <div>
      <span>{name}</span>
      <span>{age}</span>
    </div>
  );
}
<GenericList
  data={[{ name: 'John', age: 30 }, { name: 'Jane', age: 25 }]}
  render={UserListItem}
/>;

GenericList 组件的 Props 类型中,data 可以是任意类型 Trender 函数接收 T 类型的参数并返回 React 节点。这样可以实现通用的列表组件,根据传入的不同数据类型和渲染函数来展示不同的内容。

条件类型在工具函数库开发中的应用

在开发工具函数库时,条件类型能够帮助我们定义更智能的类型。

type Maybe<T> = T | null | undefined;
type IsMaybe<T> = T extends Maybe<infer U> ? true : false;
function unwrap<T>(value: Maybe<T>): T {
  if (value === null || value === undefined) {
    throw new Error('Unwrap error: value is null or undefined');
  }
  return value;
}
let num: Maybe<number> = 123;
let isMaybeNum: IsMaybe<typeof num>; // true
let unwrappedNum = unwrap(num); // number

Maybe 类型表示一个值可能是 T 类型,也可能是 nullundefinedIsMaybe 用于判断一个类型是否是 Maybe 类型。unwrap 函数用于从 Maybe 类型的值中获取实际的值,如果值为 nullundefined 则抛出错误。

条件类型的高级应用 - 类型体操

通过组合多个条件类型和其他类型操作符,我们可以实现复杂的类型计算,这通常被称为 “类型体操”。

type First<T extends any[]> = T extends [infer First, ...infer Rest] ? First : never;
type Rest<T extends any[]> = T extends [infer First, ...infer Rest] ? Rest : never;
type Pop<T extends any[]> = T extends [...infer Rest, infer Last] ? Rest : never;
type Push<T extends any[], U> = [...T, U];
type Shift<T extends any[]> = T extends [infer First, ...infer Rest] ? Rest : never;
type Unshift<T extends any[], U> = [U, ...T];
type Reverse<T extends any[]> = T extends []
  ? []
  : Push<Reverse<Rest<T>>, First<T>>;
// 测试
type Numbers = [1, 2, 3];
type FirstNum = First<Numbers>; // 1
type RestNums = Rest<Numbers>; // [2, 3]
type PoppedNums = Pop<Numbers>; // [1, 2]
type PushedNums = Push<Numbers, 4>; // [1, 2, 3, 4]
type ShiftedNums = Shift<Numbers>; // [2, 3]
type UnshiftedNums = Unshift<Numbers, 0>; // [0, 1, 2, 3]
type ReversedNums = Reverse<Numbers>; // [3, 2, 1]

上述代码实现了一系列类似数组操作的类型,如获取数组的第一个元素 First,获取数组除第一个元素外的剩余部分 Rest,弹出数组最后一个元素 Pop,向数组末尾添加元素 Push,移除数组第一个元素 Shift,向数组开头添加元素 Unshift 以及反转数组 Reverse。这些都是通过条件类型和类型推断实现的复杂类型操作。

条件类型的局限性

尽管条件类型非常强大,但它也有一些局限性。

首先,条件类型在处理递归类型时可能会遇到栈溢出问题。例如,如果一个条件类型递归地引用自身且没有正确的终止条件,就会导致编译错误。

// 错误示例,会导致栈溢出
type InfiniteRecursion<T> = InfiniteRecursion<T> extends string ? string : never;

其次,条件类型在处理非常复杂的类型关系时,代码可读性会变差。过多的嵌套和类型操作可能使得代码难以理解和维护。

最后,TypeScript 的类型系统虽然强大,但在某些情况下无法完全模拟运行时的逻辑。例如,条件类型无法基于运行时的值进行类型判断,它只能基于静态的类型信息。

优化条件类型的使用

为了优化条件类型的使用,我们可以采取以下几点建议。

一是尽量保持条件类型的简洁性。避免过度嵌套和复杂的类型操作,将复杂的逻辑拆分成多个简单的条件类型。

二是合理使用类型别名和接口来提高代码的可读性。给条件类型起一个有意义的名字,让代码的意图更加清晰。

三是充分利用 TypeScript 的类型推断机制。减少显式类型注解,让 TypeScript 根据上下文自动推断类型,这样可以使代码更简洁,同时也减少了出错的可能性。

在前端开发中,条件类型为我们提供了强大的类型控制能力,无论是在组件开发、工具函数库编写还是复杂类型计算方面,都有着广泛的应用。尽管存在一些局限性,但通过合理的使用和优化,我们能够充分发挥条件类型的优势,提升代码的质量和可维护性。