TypeScript高级类型实战:从条件类型到模板字面量类型的类型体操

条件类型:类型层面的if-else

TypeScript的条件类型是高级类型编程的基石。语法很简单:T extends U ? X : Y。但真正在项目中用好条件类型,需要理解它的分布式特性。

基础用法——提取Promise内部类型:

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type Result1 = UnwrapPromise<Promise<string>>;     // string
type Result2 = UnwrapPromise<string>;              // string
type Result3 = UnwrapPromise<Promise<Promise<number>>>; // Promise<number>

递归版本来彻底拆解嵌套Promise:

type DeepUnwrapPromise<T> = T extends Promise<infer U>
  ? DeepUnwrapPromise<U>
  : T;

type DeepResult = DeepUnwrapPromise<Promise<Promise<Promise<number>>>>;
// number

分布式条件类型是容易踩坑的地方。当条件类型的检查对象是裸类型参数时,TypeScript会自动将联合类型展开分别判断:

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

type Result = ToArray<string | number>;  // string[] | number[]

// 如果不想分布式行为,用元组包裹
type ToArrayNoDist<T> = [T] extends [any] ? T[] : never;

type Result2 = ToArrayNoDist<string | number>;  // (string | number)[]

两种结果都有用武之地。分布式版本适合对联合类型的每个成员分别处理,非分布式版本适合把联合类型当作整体处理。

infer关键字:类型层面的模式匹配

infer让条件类型具备了”提取”能力。在extends子句中用infer声明一个类型变量,TypeScript会尝试匹配并推断类型。

实战场景:提取函数返回类型和参数类型:

type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
type ParamsOf<T> = T extends (...args: infer P) => any ? P : never;

function fetchUser(id: string): Promise<{ name: string; age: number }> {}
type FetchReturn = ReturnOf<typeof fetchUser>;
// Promise<{ name: string; age: number }>
type FetchParams = ParamsOf<typeof fetchUser>;
// [string]

更实用的场景——从API响应类型中提取数据字段:

interface ApiResponse<T> {
  code: number;
  message: string;
  data: T;
  timestamp: number;
}

type ExtractData<T> = T extends ApiResponse<infer D> ? D : never;

type UserData = ExtractData<ApiResponse<{ id: string; name: string }>>;
// { id: string; name: string }

infer的递归使用可以处理更复杂的嵌套结构。提取深层嵌套对象中指定key的类型:

type PathValue<T, P extends string> = P extends `${infer K}.${infer Rest}`
  ? K extends keyof T
    ? PathValue<T[K], Rest>
    : never
  : P extends keyof T
    ? T[P]
    : never;

interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      username: string;
      password: string;
    };
  };
}

type HostType = PathValue<Config, "database.host">;
// string
type CredUserType = PathValue<Config, "database.credentials.username">;
// string

模板字面量类型:类型安全的字符串操作

TypeScript 4.1引入的模板字面量类型,让类型系统具备了字符串拼接和解析能力。

最实用的场景——类型安全的事件系统:

type EventName<T extends string> = `on${Capitalize<T>}`;
type Handler<T> = (payload: T) => void;

interface EventMap {
  click: { x: number; y: number };
  change: { value: string };
  submit: { formData: Record<string, string> };
}

type SafeEventName = EventName<keyof EventMap & string>;
// "onClick" | "onChange" | "onSubmit"

class TypedEventEmitter {
  private handlers = new Map<string, Function[]>();

  on<K extends keyof EventMap & string>(
    event: EventName<K>,
    handler: Handler<EventMap[K]>
  ): void {
    const key = event;
    if (!this.handlers.has(key)) {
      this.handlers.set(key, []);
    }
    this.handlers.get(key)!.push(handler);
  }

  emit<K extends keyof EventMap & string>(
    event: EventName<K>,
    payload: EventMap[K]
  ): void {
    this.handlers.get(event)?.forEach(fn => fn(payload));
  }
}

// 使用——类型安全
const emitter = new TypedEventEmitter();
emitter.on("onClick", (payload) => {
  console.log(payload.x, payload.y);  // payload类型自动推断
});
// emitter.on("onFoo", ...) // 编译错误

映射类型的高级用法

映射类型配合key remapping可以实现类型层面的对象转换:

// 将所有属性变为可选,并添加_前缀
type OptionalPrefix<T> = {
  [K in keyof T as `_${K & string}`]?: T[K];
};

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

type OptionalPrefixedUser = OptionalPrefix<User>;
// { _id?: string; _name?: string; _email?: string }

// 过滤出特定类型的属性
type OnlyType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface MixedConfig {
  port: number;
  host: string;
  debug: boolean;
  timeout: number;
  name: string;
}

type NumberConfig = OnlyType<MixedConfig, number>;
// { port: number; timeout: number }

key remapping的as子句是TypeScript 4.1后的能力,它让映射类型既能改key也能过滤key。结合条件类型,几乎可以实现类型层面的任何数据转换。

类型体操实战:类型安全的路由系统

把前面学到的技术组合起来,实现一个类型安全的路由注册系统:

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type RoutePath<Method extends HttpMethod, Path extends string> = `${Method} ${Path}`;

type ExtractParams<Path extends string> = Path extends `${string}:${infer Param}/${infer Rest}`
  ? Param | ExtractParams<Rest>
  : Path extends `${string}:${infer Param}`
    ? Param
    : never;

type RouteParams<Path extends string> = {
  [K in ExtractParams<Path>]: string;
};

// 测试
type UserRoute = RouteParams<"/api/users/:userId/posts/:postId">;
// { userId: string; postId: string }

function createRouter<Path extends string>(method: HttpMethod, path: Path) {
  return {
    method,
    path,
    handler: (params: RouteParams<Path>) => {},
  };
}

const route = createRouter("GET", "/api/users/:userId/posts/:postId");
route.handler({ userId: "123", postId: "456" }); // 类型安全

TypeScript的高级类型不是炫技用的花架子。在大型前端项目中,类型编程能做的事情远比想象的多:类型安全的API层、类型安全的配置解析、类型安全的状态管理。投入在类型约束上的时间,会在后期维护中以数倍的效率回报。好的类型系统就是最好的文档,也是最可靠的自动测试。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/typescript-gao-ji-lei-xing-shi-zhan-cong-tiao-jian-lei-xing/

(0)
小编小编
上一篇 2小时前
下一篇 2小时前

相关推荐

TypeScript高级类型实战:从条件类型到模板字面量类型的类型体操

条件类型:类型层面的if-else

TypeScript的条件类型是高级类型编程的基石。语法很简单:T extends U ? X : Y。但真正在项目中用好条件类型,需要理解它的分布式特性。

基础用法——提取Promise内部类型:

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type Result1 = UnwrapPromise<Promise<string>>;     // string
type Result2 = UnwrapPromise<string>;              // string
type Result3 = UnwrapPromise<Promise<Promise<number>>>; // Promise<number>

递归版本来彻底拆解嵌套Promise:

type DeepUnwrapPromise<T> = T extends Promise<infer U>
  ? DeepUnwrapPromise<U>
  : T;

type DeepResult = DeepUnwrapPromise<Promise<Promise<Promise<number>>>>;
// number

分布式条件类型是容易踩坑的地方。当条件类型的检查对象是裸类型参数时,TypeScript会自动将联合类型展开分别判断:

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

type Result = ToArray<string | number>;  // string[] | number[]

// 如果不想分布式行为,用元组包裹
type ToArrayNoDist<T> = [T] extends [any] ? T[] : never;

type Result2 = ToArrayNoDist<string | number>;  // (string | number)[]

两种结果都有用武之地。分布式版本适合对联合类型的每个成员分别处理,非分布式版本适合把联合类型当作整体处理。

infer关键字:类型层面的模式匹配

infer让条件类型具备了”提取”能力。在extends子句中用infer声明一个类型变量,TypeScript会尝试匹配并推断类型。

实战场景:提取函数返回类型和参数类型:

type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
type ParamsOf<T> = T extends (...args: infer P) => any ? P : never;

function fetchUser(id: string): Promise<{ name: string; age: number }> {}
type FetchReturn = ReturnOf<typeof fetchUser>;
// Promise<{ name: string; age: number }>
type FetchParams = ParamsOf<typeof fetchUser>;
// [string]

更实用的场景——从API响应类型中提取数据字段:

interface ApiResponse<T> {
  code: number;
  message: string;
  data: T;
  timestamp: number;
}

type ExtractData<T> = T extends ApiResponse<infer D> ? D : never;

type UserData = ExtractData<ApiResponse<{ id: string; name: string }>>;
// { id: string; name: string }

infer的递归使用可以处理更复杂的嵌套结构。提取深层嵌套对象中指定key的类型:

type PathValue<T, P extends string> = P extends `${infer K}.${infer Rest}`
  ? K extends keyof T
    ? PathValue<T[K], Rest>
    : never
  : P extends keyof T
    ? T[P]
    : never;

interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      username: string;
      password: string;
    };
  };
}

type HostType = PathValue<Config, "database.host">;
// string
type CredUserType = PathValue<Config, "database.credentials.username">;
// string

模板字面量类型:类型安全的字符串操作

TypeScript 4.1引入的模板字面量类型,让类型系统具备了字符串拼接和解析能力。

最实用的场景——类型安全的事件系统:

type EventName<T extends string> = `on${Capitalize<T>}`;
type Handler<T> = (payload: T) => void;

interface EventMap {
  click: { x: number; y: number };
  change: { value: string };
  submit: { formData: Record<string, string> };
}

type SafeEventName = EventName<keyof EventMap & string>;
// "onClick" | "onChange" | "onSubmit"

class TypedEventEmitter {
  private handlers = new Map<string, Function[]>();

  on<K extends keyof EventMap & string>(
    event: EventName<K>,
    handler: Handler<EventMap[K]>
  ): void {
    const key = event;
    if (!this.handlers.has(key)) {
      this.handlers.set(key, []);
    }
    this.handlers.get(key)!.push(handler);
  }

  emit<K extends keyof EventMap & string>(
    event: EventName<K>,
    payload: EventMap[K]
  ): void {
    this.handlers.get(event)?.forEach(fn => fn(payload));
  }
}

// 使用——类型安全
const emitter = new TypedEventEmitter();
emitter.on("onClick", (payload) => {
  console.log(payload.x, payload.y);  // payload类型自动推断
});
// emitter.on("onFoo", ...) // 编译错误

映射类型的高级用法

映射类型配合key remapping可以实现类型层面的对象转换:

// 将所有属性变为可选,并添加_前缀
type OptionalPrefix<T> = {
  [K in keyof T as `_${K & string}`]?: T[K];
};

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

type OptionalPrefixedUser = OptionalPrefix<User>;
// { _id?: string; _name?: string; _email?: string }

// 过滤出特定类型的属性
type OnlyType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface MixedConfig {
  port: number;
  host: string;
  debug: boolean;
  timeout: number;
  name: string;
}

type NumberConfig = OnlyType<MixedConfig, number>;
// { port: number; timeout: number }

key remapping的as子句是TypeScript 4.1后的能力,它让映射类型既能改key也能过滤key。结合条件类型,几乎可以实现类型层面的任何数据转换。

类型体操实战:类型安全的路由系统

把前面学到的技术组合起来,实现一个类型安全的路由注册系统:

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type RoutePath<Method extends HttpMethod, Path extends string> = `${Method} ${Path}`;

type ExtractParams<Path extends string> = Path extends `${string}:${infer Param}/${infer Rest}`
  ? Param | ExtractParams<Rest>
  : Path extends `${string}:${infer Param}`
    ? Param
    : never;

type RouteParams<Path extends string> = {
  [K in ExtractParams<Path>]: string;
};

// 测试
type UserRoute = RouteParams<"/api/users/:userId/posts/:postId">;
// { userId: string; postId: string }

function createRouter<Path extends string>(method: HttpMethod, path: Path) {
  return {
    method,
    path,
    handler: (params: RouteParams<Path>) => {},
  };
}

const route = createRouter("GET", "/api/users/:userId/posts/:postId");
route.handler({ userId: "123", postId: "456" }); // 类型安全

TypeScript的高级类型不是炫技用的花架子。在大型前端项目中,类型编程能做的事情远比想象的多:类型安全的API层、类型安全的配置解析、类型安全的状态管理。投入在类型约束上的时间,会在后期维护中以数倍的效率回报。好的类型系统就是最好的文档,也是最可靠的自动测试。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/typescript-gao-ji-lei-xing-shi-zhan-cong-tiao-jian-lei-xing/

(0)
小编小编
上一篇 2小时前
下一篇 2小时前

相关推荐