1

I am trying to create custom types for calling an API. I have a method that retrieves stock quotes. This method takes in a string array of symbols. I expect the return type to be a record with keys being those symbols passed in. Currently I have it setup like this:

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

Calling it like:

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

works great with autocomplete and everything.

But, say I'm calling it with a runtime variable (typed as string), it does not:

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

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

Is there a way around this so that Typescript knows for sure that quotes[stockSymbol] exists?

Optional chaining is an option here of course (quotes[stockSymbol]?.askPrice) but I feel like Typescript should know that property exists on quotes due to how it's typed. But I'm obviously missing something.

SpellChucker
  • 450
  • 5
  • 18
  • 2
    Does this answer your question? [How can I solve the error 'TS2532: Object is possibly 'undefined'?](https://stackoverflow.com/questions/54884488/how-can-i-solve-the-error-ts2532-object-is-possibly-undefined) – Zac Anger Aug 21 '23 at 02:36
  • 2
    Please consider [edit]ing the code to be a [mre] that demonstrates what you're talking about when we copy and paste it into our own IDEs. Right now there are undeclared types, methods detached from classes, and the actual problem you're having is mentioned but not reproduced. – jcalz Aug 21 '23 at 02:38
  • could you show a bit more of `getSymbolsQuotes` function? – khewdev Aug 21 '23 at 05:55
  • Edited the post to make it reproducible. Sorry about that! – SpellChucker Aug 21 '23 at 12:05
  • [Are you sure this is reproducible](https://tsplay.dev/Wvyq3m)? Possibly you're using some tsconfig options other than just `--strict`? If you're using `--noUncheckedIndexedAccess` then that kind of false positive is unavoidable in general and you might as well use a type assertion or optional chaining. But *is* that's what's going on here? (Also note, using the word `symbol` to talk about strings is a bit distracting, since that's the name of a different type and would be like having a variable named `number` of type `symbol` or a variable named `boolean` of type `undefined`.) – jcalz Aug 21 '23 at 14:30
  • You're absolutely right, it's the `noUncheckedIndexAccess`. Thank you! Added that in and totally forgot about it – SpellChucker Aug 21 '23 at 23:42
  • So what should we do here? Should I write up an answer saying that you might want to turn off `--noUncheckedIndexedAccess` unless you find its benefits outweigh its disadvantages? Or is this no longer an issue? Or am I missing something? – jcalz Aug 22 '23 at 00:01
  • I think an answer stating this is caused by `noUncheckedIndexedAccess` and it may be beneficial to turn off in this case. It is an issue with the flag on, and there's no real way around it without using `!` or `?` or some other type assertion with less flexibility. – SpellChucker Aug 22 '23 at 01:00

1 Answers1

1

TypeScript doesn't have any real way to track the identity of an unknown but constant string in the type system. You can make the compiler too loose or too strict, but accuracy is not possible.

If you know the literal type of a string like const key = "a", then you can say that an object has a type like declare const obj: Record<typeof key, number> and it will evaluate to {a: string} which has a specific known property at the key key. You can evaluate obj[key] and know that you're getting a number, and if you try to evaluate obj[otherKey] for some other random key string otherKey, the compiler will catch that as a mistake:

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>'.

But if all you know is that it's a string, like const key = getRandomString(), there's no way to encode that an object obj has a known property at the key key without saying the same thing about every string-valued key. That is, declare const obj: Record<typeof key, number> evaluates to {[k: string]: number}, with a string index signature. You can evaluate obj[key], but the compiler cannot tell that apart from obj[otherKey]. By default, the compiler will just let you do both without complaint and pretend that the outcome is number, even though the latter will almost certainly result in undefined and runtime errors aren't far behind:

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!

For a while this was about as good as it got, but there were many requests for protection against this sort of mistake. And eventually it was implemented: TypeScript 4.1 introduced the --noUncheckedIndexedAccess compiler option, which, when enabled, adds undefined to the domain of values read from (but not written to) index signatures. But if you enable it, now you get the opposite problem:

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

Even obj[key] is considered to be possibly undefined. So instead of a false negative where unsafe code is permitted, you get a false positive where safe code is prohibited. The compiler still has no idea that key is safe and otherKey is unsafe. They're both just of type string. The compiler only sees their types, not their identities.

Indeed this inability to tell the difference between safe and unsafe indexes is why --noUncheckedIndexedAccess is not included in the "standard" --strict suite of compiler options. Doing so would cause a lot of perfectly safe real-world code to start producing errors, a big breaking change. And if everyone began seeing this error all over the place, many would decide to use non-null assertions (!) to suppress it, and eventually just do so out of habit, which completely defeats the purpose of the flag in the first place.

The flag is there for those who want it. If you think the benefits of using it outweigh the disadvantages, you should feel free to enable it, but if it annoys you, you should leave it off. Either way, you'll need to inspect index signature property reads yourself and decide whether they are safe or unsafe, since the compiler unfortunately cannot.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360