TypeScript自定义API类型:解决动态符号的属性存在性校验问题
问题描述
我正在为API调用创建自定义类型,编写了一个获取股票报价的方法,该方法接收字符串类型的股票符号数组,期望返回类型为以传入符号为键的Record对象。当前代码实现如下:
import axios from "axios"; export interface IQuote { readonly askPrice: number; readonly bidPrice: number; readonly closePrice: number; readonly expirationDate: number; readonly lastPrice: number; readonly openPrice: number; } export type GetSymbolsQuotesResponse<T extends string[]> = Record< T[number], IQuote >; export async function getSymbolsQuotes<TSymbol extends string>( symbols: TSymbol[] ): Promise<GetSymbolsQuotesResponse<TSymbol[]>> { const { data } = await axios.get<GetSymbolsQuotesResponse<TSymbol[]>>( `/marketdata/quotes`, { params: { symbol: symbols.join(",") } } ); return data; }
当传入静态定义的符号(如"SPX")时,自动补全正常工作:
const stockSymbol = "SPX"; const quotes = await getSymbolsQuotes([stockSymbol]); // 自动补全正常: quotes["SPX"].lastPrice;
但传入类型为string的运行时变量时,会出现“Object possibly undefined”错误:
async function getQuotes(stockSymbol: string) { const quotes = await getSymbolsQuotes([stockSymbol]); // 报错:Object possibly undefined quotes[stockSymbol].askPrice; }
是否有办法让TypeScript确定quotes[stockSymbol]一定存在?可选链是备选方案,但我认为TypeScript应根据类型定义识别该属性存在,想找到更合适的解决方式。
解决方案
问题核心是:当传入宽泛的string类型变量时,TypeScript会将泛型TSymbol推断为string,此时Record<string, IQuote>无法保证任意字符串键都存在于返回对象中,因此触发“可能未定义”的提示。以下是几种针对性的解决方式:
1. 用泛型约束缩小类型范围
修改函数的泛型和返回类型定义,让TSymbol代表单个字符串字面量而非宽泛的string,同时配合类型守卫确保传入的符号合法:
// 修改后的函数 export async function getSymbolsQuotes<TSymbol extends string>( symbols: readonly TSymbol[] ): Promise<Record<TSymbol, IQuote>> { const { data } = await axios.get<Record<TSymbol, IQuote>>( `/marketdata/quotes`, { params: { symbol: symbols.join(",") } } ); return data; } // 调用时添加类型守卫 function isValidSymbol(s: string): s is string { // 可添加实际校验逻辑,比如检查是否在合法股票代码列表中 return true; } async function getQuotes(stockSymbol: string) { if (!isValidSymbol(stockSymbol)) throw new Error("无效的股票符号"); const quotes = await getSymbolsQuotes([stockSymbol]); quotes[stockSymbol].askPrice; // 不再报错 }
如果能提前确定符号的可能取值,也可以直接用字面量联合类型断言:
async function getQuotes(stockSymbol: string) { const quotes = await getSymbolsQuotes([stockSymbol as "SPX" | "AAPL"]); quotes[stockSymbol as "SPX"].askPrice; }
2. 使用NoInfer锁定泛型推断(TypeScript 5.4+)
通过NoInfer避免泛型被宽泛地推断为string,强制TypeScript以传入变量的具体类型为准:
// 若没有type-fest依赖,可自行实现NoInfer:type NoInfer<T> = [T][T extends any ? 0 : never] import { NoInfer } from "type-fest"; export async function getSymbolsQuotes<TSymbol extends string>( symbols: NoInfer<TSymbol>[] ): Promise<Record<TSymbol, IQuote>> { const { data } = await axios.get<Record<TSymbol, IQuote>>( `/marketdata/quotes`, { params: { symbol: symbols.join(",") } } ); return data; } // 调用时显式指定泛型参数 async function getQuotes(stockSymbol: string) { const quotes = await getSymbolsQuotes<typeof stockSymbol>([stockSymbol]); quotes[stockSymbol].askPrice; }
3. 修改返回类型为只读Record
将返回类型改为Readonly<Record<TSymbol, IQuote>>,可以帮助TypeScript更准确地识别键的确定性:
export async function getSymbolsQuotes<TSymbol extends string>( symbols: TSymbol[] ): Promise<Readonly<Record<TSymbol, IQuote>>> { const { data } = await axios.get<Readonly<Record<TSymbol, IQuote>>>( `/marketdata/quotes`, { params: { symbol: symbols.join(",") } } ); return data; }
结合类型守卫或断言使用,即可消除报错。
内容的提问来源于stack exchange,提问作者SpellChucker
相关产品推荐
相关产品推荐

