我正在try 为调用API创建自定义类型.我有一个检索股票报价的方法.此方法接受符号的字符串array.我希望返回类型是一个记录,键是那些传入的符号.目前我的设置如下:

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;
}

它是这样称呼的:

const stockSymbol = "SPX";
const quotes = await getSymbolsQuotes([stockSymbol]);
// Lovely with autocomplete:
quotes["SPX"].lastPrice;

与自动补全和其他功能配合使用时效果很好.

但是,假设我使用运行时变量(类型为string)调用它,它不会:

async function getQuotes(stockSymbol: string) {
  const quotes = await getSymbolsQuotes([stockSymbol]);

  // Object possibly undefined:
  quotes[stockSymbol].askPrice;
}

有没有办法绕过这一点,让TypeScrip确信quotes[stockSymbol]的存在?

当然,可选链接在这里是一个选项(quotes[stockSymbol]?.askPrice),但我觉得TypeScrip应该知道属性存在于quotes上,因为它是如何键入的.但我显然漏掉了什么.

推荐答案

TypeScrip没有任何真正的方法来跟踪类型系统中未知但恒定的字符串的identity.您可以将编译器设置得过于宽松或过于严格,但不可能达到精确度.

如果你知道像const key = "a"这样的字符串的literal type,那么你可以说一个对象有一个像declare const obj: Record<typeof key, number>这样的类型,它的计算结果是{a: string},它在键key处有一个特定的已知属性.您可以计算obj[key],并且知道您得到的是number,如果您try 为其他随机密钥字符串otherKey计算obj[otherKey],编译器会将其视为错误:

const key = "a";
const obj: Record<typeof key, number> = { [key]: Math.PI };
const otherKey = getRandomString();

obj[key].toFixed(2); // okay
obj[otherKey].toFixed(2); // error! 
// Expression of type 'string' can't be used to index type 'Record<"a", number>'.

但如果你只知道它是一个string,就像const key = getRandomString()一样,就没有办法编码一个对象obj在键key处有一个已知的属性,而不是对everystring值键说同样的事情.也就是说,declare const obj: Record<typeof key, number>的计算结果为{[k: string]: number},字符串为index signature.您可以对obj[key]求值,但编译器无法区分obj[otherKey].默认情况下,编译器只会让您毫无怨言地执行这两项操作,并假装结果为number,即使后者几乎肯定会导致undefined,并且运行时错误也不会太远:

const key = getRandomString();
const obj: Record<typeof key, number> = { [key]: Math.PI };
const otherKey = getRandomString();
obj[key].toFixed(2); // okay
obj[otherKey].toFixed(2); // okay, but very likely RUNTIME ERROR!

有一段时间,这几乎是它得到的最好的结果,但有很多人要求保护自己免受此类错误的影响.最终它被实现了:TypeScrip4.1引入了the --noUncheckedIndexedAccess compiler option,当被启用时,它将向从索引签名读取(但不写入)的值域添加undefined.但如果你启用它,现在你会遇到相反的问题:

const key = getRandomString();
const obj: Record<typeof key, number> = { [key]: Math.PI };
const otherKey = getRandomString();
obj[key].toFixed(2); // error, object is possibly undefined
obj[otherKey].toFixed(2); // error, object is possibly undefined

即使是obj[key]也被认为可能是undefined.因此,在允许不安全代码的情况下不是假阴性,而是在禁止安全代码的情况下得到假阳性.编译器仍然不知道key是安全的,而otherKey是不安全的.他们都是string型的.编译器只能看到它们的类型,而不能看到它们的标识.

事实上,这种无法区分安全指数和不安全指数的能力,就是为什么the "standard" --strict suite of compiler options指数中没有包括--noUncheckedIndexedAccess指数的原因.这样做将导致许多完全安全的现实世界代码开始产生错误,这是一个巨大的突破性变化.如果每个人都开始在各地看到这个错误,许多人会决定使用non-null assertions (!)来 suppress 它,最终只是出于习惯这样做,这从一开始就完全违背了国旗的目的.

这面旗帜是为那些想要它的人准备的.如果你认为使用它的好处大于坏处,你应该自由地启用它,但如果它惹恼了你,你应该把它关掉.无论采用哪种方法,您都需要自己判断索引签名属性读数,并确定它们是安全的还是不安全的,因为不幸的是编译器不能.

Playground link to code

Typescript相关问答推荐

在角形加载组件之前无法获取最新令牌

属性的类型,该属性是类的键,其值是所需类型

在泛型类型中对子代执行递归时出错

webpack错误:模块找不到.当使用我的其他库时,

基于平台的重定向,Angular 为16

具有动态键的泛型类型

如何使用一个字段输入对象,其中每个键只能是数组中对象的id属性,而数组是另一个字段?

当方法参数和返回类型不相互扩展时如何从类型脚本中的子类推断类型

如何以Angular 形式显示验证错误消息

使用2个泛型类型参数透明地处理函数中的联合类型

从子类构造函数推断父类泛型类型

分解对象时出现打字错误

TypeScrip:如何缩小具有联合类型属性的对象

如何将通用接口的键正确设置为接口的键

在Thunk中间件中输入额外参数时Redux工具包的打字脚本错误

类型脚本中参数为`T`和`t|unfined`的重载函数

有没有什么方法可以使用ProvideState()提供多个削减器作为根减减器

如何将元素推入对象中的类型化数组

传入类型参数<;T>;变容函数

从 npm 切换到 pnpm 后出现Typescript 错误