如何用TypeScript创建基于字符串字面量确定返回类型的类型安全泛型API SDK
问题描述
希望用TypeScript创建一个泛型API SDK,让返回类型由传入的字符串字面量参数决定。同时基于字符串到对象类型的预设映射,实现:
- a) 确定返回值的类型
- b) 确保函数具备最高程度的类型安全性
示例代码
import Axios from "axios"; export const axios = Axios.create(); type Foo = { id: string; name: string; foo: number; } type Bar = { id: string; name: string; bar: number; } type FooResponse = { response: Foo[]; } type BarResponse = { response: Bar[]; } type FooBarMap = { foo: FooResponse; bar: BarResponse; } const getFooBar = async <T extends keyof FooBarMap>(endpoint: T, query: Record<string, any> = {}) => { const records: FooBarMap[T]["response"] = []; const response = await axios.get<FooBarMap[T]>(`/${endpoint}`, query); const data = response.data.response; records.push(...data); return records; }
遇到的错误
类型为'Foo | Bar'的参数无法赋值给类型为'Foo & Bar'的参数。
类型'Foo'无法赋值给类型为'Foo & Bar'。
类型'Foo'中缺少类型'Bar'所需的属性'bar'。
解决方案
错误原因
TypeScript处理泛型数组的push操作时,无法精准推断data的具体类型是Foo[]还是Bar[],而是将其推断为(Foo | Bar)[]。但records的类型是FooBarMap[T]["response"](即T为foo时是Foo[],为bar时是 Hotels获取充态nett现将体现[具体ossJava classic Acceler drug](https://github.com/microsoft/TypeScript/issues/30655),两种类型无法直接兼容,因此报错。
修复代码
方案1:用类型断言明确data类型
const getFooBar = async <T extends keyof FooBarMap>(endpoint: T, query: Record<string, any> = {}) => { const records: FooBarMap[T]["response"] = []; // 注意:axios.get的第二个参数是配置对象,query需放在params字段中 const response = await axios.get<FooBarMap[T]>(`/${endpoint}`, { params: query }); // 断言data类型与records完全匹配 const data = response.data.response as FooBarMap[T]["response"]; records.push(...data); return records; }
方案2:简化逻辑直接返回响应数据
如果不需要额外的数组拼接逻辑,直接返回响应数据可彻底避免类型推断问题:
const getFooBar = async <T extends keyof FooBarMap>(endpoint: T, query: Record<string, any> = {}) => { const response = await axios.get<FooBarMap[T]>(`/${endpoint}`, { params: query }); return response.data.response; }
提升类型安全性的优化点
严格约束Query类型:为每个端点定义专属的Query类型,避免传参错误:
// 为每个endpoint绑定对应的Query类型 type EndpointQueryMap = { foo: { page?: number; limit?: number }; bar: { category?: string }; } const getFooBar = async <T extends keyof FooBarMap>( endpoint: T, query: EndpointQueryMap[T] = {} as EndpointQueryMap[T] ) => { const response = await axios.get<FooBarMap[T]>(`/${endpoint}`, { params: query }); return response.data.response; }调用时传入不符合端点要求的Query会直接触发编译报错。
避免
any类型:替换Record<string, any>为具体类型或unknown,配合类型守卫进一步校验数据。显式声明返回类型:让函数类型更清晰,便于维护:
const getFooBar = async <T extends keyof FooBarMap>( endpoint: T, query: EndpointQueryMap[T] = {} as EndpointQueryMap[T] ): Promise<FooBarMap[T]["response"]> => { const response = await axios.get<FooBarMap[T]>(`/${endpoint}`, { params: query }); return response.data.response; }
问题解答
a) 确定返回值类型:通过
FooBarMap[T]["response"]的索引访问类型,完全可以根据传入的endpoint字符串字面量确定返回值的具体类型。比如调用getFooBar('foo')时,返回类型会被推断为Promise<Foo[]>;调用getFooBar('bar')时则为Promise<Bar[]>。b) 最高程度的类型安全性:通过约束
endpoint为keyof FooBarMap、绑定端点专属Query类型、避免any、显式类型声明等优化,可在编译阶段捕获大部分类型错误(如传入不存在的 lent随意 fileH一组...)excel yet优秀画面 graveZ栓蛇),比如无效端点、错误参数、返回值误用等,实现高度的类型安全。
内容的提问来源于stack exchange,提问作者Tyler Clendenin

