1

How can a typed array of strings be used as a union of literal types based on their actual values? On a high level, I have a typed array like ['hello', 'world'] and what I want to infer from that is a new type of 'hello' | 'world'.

const someArray: Readonly<Array<string>> = ['hello', 'world'] as const;
type SomeType = typeof someArray[number]; // string

SomeType will now be inferred to be string instead of the union. How can I infer a union of the literal types?

This question is very similar to TypeScript array to string literal type with the difference, that the array is typed. I cannot remove that. The code below would work, but someArray is actually typed as Array<string>.

const someArray = ['hello', 'world'] as const;
type SomeType = typeof someArray[number]; // 'hello' | 'world'

Is there any way to narrow the inferred type accordingly?

edit: This example is obviously simplified. I use a third-party library that expects an array of objects. These are typed by the third-party and I cannot change that without loosing type support. I realize that it would work without a type, but I cannot realistically remove that.

str
  • 42,689
  • 17
  • 109
  • 127
  • Does this answer your question? [Typescript derive union type from tuple/array values](https://stackoverflow.com/questions/45251664/typescript-derive-union-type-from-tuple-array-values) – Harun Yilmaz Nov 04 '21 at 07:56
  • 3
    You've already found all of the answers - if all the compiler has to go on is `string[]`, it obviously doesn't have the information to create the union from. – jonrsharpe Nov 04 '21 at 07:59
  • `Readonly>` has higher precedence than `as const`. – captain-yossarian from Ukraine Nov 04 '21 at 08:00
  • @HarunYilmaz No it does not. I know that it would work without a type, but I cannot realistically remove it (see my edit). My question was whether and how it can be done *with* a type. – str Nov 04 '21 at 08:03
  • 1
    It can't, adding the type explicitly overrides whatever the compile infers. You'd have to explicitly type the tuple (either by changing the type or explicitly casting to tuple) or remove the type altogether. – pascalpuetz Nov 04 '21 at 08:18

1 Answers1

2

You can't get TypeScript to infer something that's in direct contradiction to what it's been told explicitly (Readonly<Array<string>>). That type annotation wins the day.

The fix is, as you've indicated, to remove the type annotation. But you've said you can't do that.

Since you can't remove the type annotation — e.g., the array is defined by code you don't control — then you have no choice but to duplicate the list of possible values in the type, which leaves you open to maintenance issues when the original changes and you don't update the duplicate. To mitigate that, you can add a runtime check to handle the case where the original is changed so they no longer match:

const duplicatedArray = ['hello', 'world'] as const;
type SomeType = typeof duplicatedArray[number]; // 'hello' | 'world'
if ( someArray.length !== duplicatedArray.length ||
     someArray.some((e, i) => e !== duplicatedArray[i])) {
    throw new Error(`'someArray' and 'duplicatedArray' no longer match`);
}

(The runtime part of that might be in an automated test rather than the main code.)

Very much a second- or third-best solution, but if you have no choice, it's at least something.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • OK, thanks for the clarification. As this does not (realistically) seem to be possible, I'm considering to just create the union type manually and narrow the type definition of the data structure accordingly. Even though that makes it slightly repetitive. – str Nov 04 '21 at 08:30
  • 1
    Yeah, it depends on how complex the original is. The runtime check is realistic in many scenarios, but not all. Where possible, it's useful for catching errors, but where it isn't, you have to fall back on manual processes... – T.J. Crowder Nov 04 '21 at 08:35