Skip to content

TypeScript高级类型技巧

TypeScript的类型系统非常强大,提供了许多高级特性来解决复杂的类型问题。

条件类型

条件类型允许我们基于类型关系创建新的类型,类似于JavaScript中的条件表达式。

基本条件类型

条件类型的基本语法是 T extends U ? X : Y,意思是"如果类型T可以赋值给类型U,则结果类型为X,否则为Y":

typescript
// 基本条件类型
type IsString<T> = T extends string ? true : false;

// 使用条件类型
type A = IsString<string>;  // true
type B = IsString<number>;  // false
type C = IsString<"hello">; // true(字面量类型也是string的子类型)

分配条件类型

当条件类型应用于联合类型时,它会分配到联合的每个成员:

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

// 分配条件类型
type StringOrNumberArray = ToArray<string | number>;
// 等同于: string[] | number[]

// 避免分配行为
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result = ToArrayNonDist<string | number>;
// (string | number)[]

条件类型中的类型推断

使用infer关键字,我们可以在条件类型中推断和捕获类型的一部分:

typescript
// 从函数类型中提取返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

function greet(): string {
  return "你好,世界!";
}

type GreetReturn = ReturnType<typeof greet>; // string

// 从数组类型中提取元素类型
type ElementType<T> = T extends (infer U)[] ? U : never;
type NumberType = ElementType<number[]>; // number
type StringType = ElementType<string[]>; // string

// 从Promise中提取值类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type ResolvedType = UnwrapPromise<Promise<string>>; // string
type NonPromiseType = UnwrapPromise<number>; // number

条件类型链

条件类型可以链式使用,类似于if-else if-else链:

typescript
type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

// 使用条件类型链
type T0 = TypeName<string>;  // "string"
type T1 = TypeName<42>;      // "number"
type T2 = TypeName<true>;    // "boolean"
type T3 = TypeName<() => void>; // "function"
type T4 = TypeName<{}>; // "object"

映射类型

映射类型允许我们基于旧类型创建新类型,通过转换每个属性。

基本映射类型

基本映射类型语法使用[P in keyof T]来遍历类型的所有属性:

typescript
// 将所有属性设为只读
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

// 将所有属性设为可选
type Partial<T> = {
  [P in keyof T]?: T[P];
};

interface User {
  id: number;
  name: string;
  email: string;
}

// 使用映射类型
type ReadonlyUser = Readonly<User>;
// { readonly id: number; readonly name: string; readonly email: string; }

type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; }

键重映射

TypeScript 4.1引入了键重映射,允许我们在映射类型中转换属性名:

typescript
// 添加前缀
type Prefix<T, P extends string> = {
  [K in keyof T as `${P}${string & K}`]: T[K];
};

// 过滤属性
type FilterByValueType<T, ValueType> = {
  [K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};

interface Person {
  name: string;
  age: number;
  address: string;
  isActive: boolean;
}

// 使用键重映射
type PrefixedPerson = Prefix<Person, "get">;
// { getName: string; getAge: number; getAddress: string; getIsActive: boolean; }

type StringProperties = FilterByValueType<Person, string>;
// { name: string; address: string; }

映射修饰符

我们可以使用+-修饰符来添加或移除readonly?修饰符:

typescript
// 移除只读属性
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

// 移除可选属性
type Required<T> = {
  [P in keyof T]-?: T[P];
};

// 添加只读和可选
type ReadonlyAndPartial<T> = {
  +readonly [P in keyof T]+?: T[P];
};

interface Config {
  readonly host: string;
  port: number;
  timeout: number;
  retries?: number;
}

// 使用映射修饰符
type MutableConfig = Mutable<Config>;
// { host: string; port: number; timeout: number; retries?: number; }

type RequiredConfig = Required<Config>;
// { host: string; port: number; timeout: number; retries: number; }

type SafeConfig = ReadonlyAndPartial<Config>;
// { readonly host?: string; readonly port?: number; readonly timeout?: number; readonly retries?: number; }

索引类型

索引类型允许我们在编译时检查使用动态属性名的代码。

索引类型查询操作符

keyof操作符获取一个对象类型的所有键的联合:

typescript
interface User {
  id: number;
  name: string;
  email: string;
}

// 使用keyof
type UserKeys = keyof User; // "id" | "name" | "email"

// 使用索引类型查询
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user: User = {
  id: 1,
  name: "张三",
  email: "zhangsan@example.com"
};

// 类型安全的属性访问
const userName = getProperty(user, "name"); // 类型是string
const userId = getProperty(user, "id"); // 类型是number

// 错误:参数"age"不能赋给参数"K"
// getProperty(user, "age");

索引访问类型

使用T[K]语法可以查找类型T上属性K的类型:

typescript
interface Person {
  name: string;
  age: number;
  address: {
    street: string;
    city: string;
    country: string;
  };
}

// 使用索引访问类型
type PersonName = Person["name"]; // string
type PersonAge = Person["age"]; // number
type AddressProperty = Person["address"]; // { street: string; city: string; country: string; }
type CityType = Person["address"]["city"]; // string

// 使用联合类型作为索引
type NameOrAge = Person["name" | "age"]; // string | number

// 使用keyof
type AllPersonProps = Person[keyof Person]; // string | number | { street: string; city: string; country: string; }

类型工具与组合

TypeScript提供了许多内置工具类型,我们也可以创建自定义工具类型。

内置工具类型

TypeScript提供了许多有用的工具类型:

typescript
// Partial - 将所有属性变为可选
interface User {
  id: number;
  name: string;
  email: string;
}

type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; }

// Required - 将所有属性变为必需
interface Config {
  host?: string;
  port?: number;
  timeout?: number;
}

type RequiredConfig = Required<Config>;
// { host: string; port: number; timeout: number; }

// Readonly - 将所有属性变为只读
type ReadonlyUser = Readonly<User>;
// { readonly id: number; readonly name: string; readonly email: string; }

// Record - 创建具有指定键和值类型的对象类型
type PageInfo = Record<string, { title: string; content: string }>;
// { [key: string]: { title: string; content: string } }

// Pick - 从类型中选择指定的属性
type UserBasicInfo = Pick<User, "id" | "name">;
// { id: number; name: string; }

// Omit - 从类型中排除指定的属性
type UserWithoutEmail = Omit<User, "email">;
// { id: number; name: string; }

// Exclude - 从联合类型中排除指定的类型
type Status = "pending" | "approved" | "rejected";
type NonRejectedStatus = Exclude<Status, "rejected">;
// "pending" | "approved"

// Extract - 从联合类型中提取指定的类型
type PositiveStatus = Extract<Status, "approved" | "pending">;
// "approved" | "pending"

// NonNullable - 排除null和undefined
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;
// string

// ReturnType - 获取函数返回类型
function createUser(name: string, email: string): User {
  return { id: Date.now(), name, email };
}

type CreateUserReturn = ReturnType<typeof createUser>;
// User

// Parameters - 获取函数参数类型的元组
type CreateUserParams = Parameters<typeof createUser>;
// [name: string, email: string]

// ConstructorParameters - 获取构造函数参数类型的元组
class Person {
  constructor(name: string, age: number) {}
}

type PersonConstructorParams = ConstructorParameters<typeof Person>;
// [name: string, age: number]

// InstanceType - 获取构造函数实例类型
type PersonInstance = InstanceType<typeof Person>;
// Person

自定义工具类型

我们可以创建自定义工具类型来满足特定需求:

typescript
// 深度Partial
type DeepPartial<T> = T extends object ? {
  [P in keyof T]?: DeepPartial<T[P]>;
} : T;

interface NestedObject {
  name: string;
  settings: {
    theme: string;
    notifications: {
      email: boolean;
      sms: boolean;
      push: boolean;
    };
  };
}

// 使用DeepPartial
const partialSettings: DeepPartial<NestedObject> = {
  name: "我的应用",
  settings: {
    notifications: {
      email: true
      // sms和push是可选的
    }
    // theme是可选的
  }
};

// 只读递归
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

// 非空类型
type NonNullableProperties<T> = {
  [P in keyof T]: NonNullable<T[P]>;
};

// 函数属性
type FunctionPropertyNames<T> = {
  [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

// 非函数属性
type NonFunctionPropertyNames<T> = {
  [K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];

interface Example {
  name: string;
  age: number;
  greet: () => void;
  calculate: (x: number) => number;
}

type FuncProps = FunctionPropertyNames<Example>; // "greet" | "calculate"
type DataProps = NonFunctionPropertyNames<Example>; // "name" | "age"

类型守卫与类型收窄

类型守卫允许我们在运行时检查类型,并在特定的代码块中收窄类型。

用户定义的类型守卫

使用类型谓词(parameterName is Type)创建自定义类型守卫:

typescript
// 定义类型
interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

// 类型守卫函数
function isFish(pet: Bird | Fish): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

// 使用类型守卫
function move(pet: Bird | Fish) {
  if (isFish(pet)) {
    // 在这个块中,TypeScript知道pet是Fish类型
    pet.swim();
  } else {
    // 在这个块中,TypeScript知道pet是Bird类型
    pet.fly();
  }
}

// 泛型类型守卫
function isArray<T>(value: unknown): value is T[] {
  return Array.isArray(value);
}

function processValue<T>(value: unknown) {
  if (isArray<number>(value)) {
    // value的类型是number[]
    value.map(n => n * 2);
  }
}

类型收窄

TypeScript提供了多种方式来收窄类型:

typescript
// typeof类型守卫
function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
    // 在这个块中,padding的类型是number
    return " ".repeat(padding) + value;
  }
  // 在这个块中,padding的类型是string
  return padding + value;
}

// instanceof类型守卫
class Bird {
  fly() {
    console.log("鸟儿飞行");
  }
}

class Fish {
  swim() {
    console.log("鱼儿游泳");
  }
}

function move(animal: Bird | Fish) {
  if (animal instanceof Bird) {
    // 在这个块中,animal的类型是Bird
    animal.fly();
  } else {
    // 在这个块中,animal的类型是Fish
    animal.swim();
  }
}

// in操作符类型守卫
interface Admin {
  name: string;
  privileges: string[];
}

interface Employee {
  name: string;
  startDate: Date;
}

type UnknownEmployee = Employee | Admin;

function printEmployeeInfo(emp: UnknownEmployee) {
  console.log(`姓名: ${emp.name}`);
  
  if ("privileges" in emp) {
    // 在这个块中,emp的类型是Admin
    console.log(`特权: ${emp.privileges.join(", ")}`);
  }
  
  if ("startDate" in emp) {
    // 在这个块中,emp的类型是Employee
    console.log(`入职日期: ${emp.startDate.toLocaleDateString()}`);
  }
}

// 可辨识联合类型
interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

interface Rectangle {
  kind: "rectangle";
  width: number;
  height: number;
}

type Shape = Circle | Square | Rectangle;

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      // 在这个case中,shape的类型是Circle
      return Math.PI * shape.radius ** 2;
    case "square":
      // 在这个case中,shape的类型是Square
      return shape.sideLength ** 2;
    case "rectangle":
      // 在这个case中,shape的类型是Rectangle
      return shape.width * shape.height;
    default:
      // 穷尽检查
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

模板字面量类型

TypeScript 4.1引入了模板字面量类型,允许我们基于字符串字面量类型创建新的字符串字面量类型。

基本模板字面量类型

模板字面量类型使用反引号语法,类似于JavaScript的模板字符串:

typescript
// 基本模板字面量类型
type World = "world";
type Greeting = `hello ${World}`; // "hello world"

// 使用联合类型
type Color = "red" | "blue" | "green";
type Quantity = "one" | "two" | "three";
type ColoredQuantity = `${Quantity} ${Color}`; 
// "one red" | "one blue" | "one green" | "two red" | ... | "three green"

// 使用类型参数
type Prefix<P extends string, T extends string> = `${P}${T}`;
type Prefixed = Prefix<"get", "Name" | "Age" | "Email">;
// "getName" | "getAge" | "getEmail"

内置字符串操作类型

TypeScript提供了几个内置的字符串操作类型:

typescript
// Uppercase - 转换为大写
type UppercaseGreeting = Uppercase<"hello">; // "HELLO"

// Lowercase - 转换为小写
type LowercaseGreeting = Lowercase<"HELLO">; // "hello"

// Capitalize - 首字母大写
type CapitalizedGreeting = Capitalize<"hello">; // "Hello"

// Uncapitalize - 首字母小写
type UncapitalizedGreeting = Uncapitalize<"Hello">; // "hello"

// 组合使用
type PropEventName<T extends string> = `on${Capitalize<T>}`;
type ButtonEvent = PropEventName<"click" | "mousedown" | "mouseup">;
// "onClick" | "onMousedown" | "onMouseup"

模板字面量的高级应用

模板字面量类型可以与其他类型特性结合使用:

typescript
// 创建对象属性的getter类型
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person {
  name: string;
  age: number;
  address: string;
}

type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; getAddress: () => string; }

// 创建事件处理器类型
type EventHandler<T extends string> = {
  [K in T as `on${Capitalize<K>}`]: (event: any) => void;
};

type ButtonEvents = EventHandler<"click" | "mouseenter" | "mouseleave">;
// { onClick: (event: any) => void; onMouseenter: (event: any) => void; onMouseleave: (event: any) => void; }

// 创建状态变更类型
type State = {
  loading: boolean;
  error: Error | null;
  data: any;
};

type SetActions<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

type StateActions = SetActions<State>;
// { setLoading: (value: boolean) => void; setError: (value: Error | null) => void; setData: (value: any) => void; }

高级类型挑战

以下是一些高级类型编程挑战,可以帮助你提升TypeScript类型系统的理解和应用能力。

实现深度只读

创建一个类型,使对象及其所有嵌套属性都变为只读:

typescript
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object 
    ? T[P] extends Function 
      ? T[P] 
      : DeepReadonly<T[P]> 
    : T[P];
};

// 测试
interface NestedObject {
  a: number;
  b: string;
  c: {
    d: boolean;
    e: {
      f: string[];
    };
  };
}

const original: NestedObject = {
  a: 1,
  b: "hello",
  c: {
    d: true,
    e: {
      f: ["world"]
    }
  }
};

const readonlyObj: DeepReadonly<NestedObject> = original;

// 以下操作都会导致错误
// readonlyObj.a = 2;
// readonlyObj.c.d = false;
// readonlyObj.c.e.f.push("!");

实现类型过滤

创建一个类型,从对象类型中过滤出符合特定条件的属性:

typescript
type FilterByValueType<T, ValueType> = {
  [K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};

// 测试
interface Mixed {
  name: string;
  age: number;
  isActive: boolean;
  tags: string[];
  metadata: {
    createdAt: Date;
  };
  process(): void;
}

type StringProps = FilterByValueType<Mixed, string>;
// { name: string; }

type FunctionProps = FilterByValueType<Mixed, Function>;
// { process: () => void; }

type ObjectProps = FilterByValueType<Mixed, object>;
// { tags: string[]; metadata: { createdAt: Date; }; }

实现联合类型转交叉类型

创建一个类型,将联合类型转换为交叉类型:

typescript
type UnionToIntersection<U> = 
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

// 测试
type Union = { a: string } | { b: number } | { c: boolean };
type Intersection = UnionToIntersection<Union>;
// { a: string } & { b: number } & { c: boolean }

// 使用交叉类型
const obj: Intersection = {
  a: "hello",
  b: 42,
  c: true
};

实现元组转联合类型

创建一个类型,将元组类型转换为联合类型:

typescript
type TupleToUnion<T extends any[]> = T[number];

// 测试
type Tuple = [string, number, boolean];
type Union = TupleToUnion<Tuple>;
// string | number | boolean

实现联合类型转元组

这是一个更复杂的挑战,将联合类型转换为元组类型:

typescript
// 注意:这是一个高级实现,需要深入理解TypeScript类型系统
type UnionToTuple<T, L = LastOf<T>> = 
  [T] extends [never] 
    ? [] 
    : [...UnionToTuple<Exclude<T, L>>, L];

type LastOf<T> = 
  UnionToIntersection<T extends any ? () => T : never> extends () => infer R 
    ? R 
    : never;

// 辅助类型
type UnionToIntersection<U> = 
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

// 测试
type Union = "a" | "b" | "c";
type Tuple = UnionToTuple<Union>;
// ["a", "b", "c"] 或其他排列,取决于实现

总结

TypeScript的高级类型系统提供了强大的工具,使我们能够创建精确、安全且可重用的类型定义。通过掌握条件类型、映射类型、索引类型、类型守卫和模板字面量类型等高级特性,你可以构建更加健壮的应用程序,减少运行时错误,并提高代码的可维护性。

随着你对TypeScript类型系统的深入理解,你将能够解决越来越复杂的类型问题,创建更加精确的类型定义,并充分利用TypeScript提供的类型安全保障。类型编程虽然有时看起来复杂,但掌握这些技巧将极大地提升你的TypeScript开发能力。

用知识点燃技术的火山