You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 17:35:00