93

I can't be the first person coming across this, but my searches have not turned up any useful leads yet. Would greatly appreciate some expert TypeScript advice.

Say I have an array:

const fruits = ["Apple", "Orange", "Pear"];

and I want to define an object mapping each fruit to some fun facts about it:

interface Facts {
    color: string,
    typicalWeight: number
}

const fruitFacts: { [key: members of fruits]: Facts } = {
    "Apple": { color: "green", typicalWeight: 150 }
    //
}

How do I do that [key: members of fruits] part?

Bonus: How do I enforce that my fruitFacts object exhaust all the keys derived from the array as well, so that it specifies facts for Apples, Oranges, and Pears in the example above.

Arash Motamedi
  • 9,284
  • 5
  • 34
  • 43
  • 1
    Do you know the exact strings at compile time? If not, you cannot define such a type. – Mikhail Burshteyn Aug 29 '18 at 20:20
  • Let's say I do. Can I avoid duplicating them though? i.e. avoid doing `type FruitName = "Apple" | "Orange"; const fruitNames : FruitName[] = ["Apple", "Orange"];` – Arash Motamedi Aug 29 '18 at 20:26
  • https://stackoverflow.com/questions/45251664/typescript-derive-union-type-from-tuple-array-values/45257357 – tokland Jan 18 '20 at 21:38

2 Answers2

171

TypeScript 3.4 added const assertions which allow for writing this as:

const fruits = ["Apple", "Orange", "Pear"] as const;
type Fruit = typeof fruits[number]; // "Apple" | "Orange" | "Pear"

With as const TypeScript infers the type of fruits above as readonly["Apple", "Orange", "Pear"]. Previously, it would infer it as string[], preventing typeof fruits[number] from producing the desired union type.

Ben Regenspan
  • 10,058
  • 2
  • 33
  • 44
  • 1
    why doesn't this work?: `const fruitTypes = ["Apple", "Orange", "Pear"];` `const fruits = fruitTypes as const;` – techguy2000 Dec 05 '19 at 21:31
  • 3
    @techguy2000 I think it's because you could have: `const fruitTypes = ["Apple", "Orange", "Pear"]; fruitTypes.push("Kiwi"); const fruits = fruitTypes as const;`. TS has no reliable way of knowing in this case that the type should now be `["Apple", "Orange", "Pear", "Kiwi"];`, so it's an unsafe pattern to allow marking it as `const` later on after the initial definition. – Ben Regenspan Dec 06 '19 at 22:16
  • 2
    It still doesn't work when I freeze the array: `const fruitTypes = Object.freeze(["Apple", "Orange", "Pear"]);` I really hope some variation of this will work... – techguy2000 Dec 09 '19 at 06:06
  • 2
    @techguy2000 that might be worthwhile to open up as a feature suggestion in the TS issues tracker, it does seem like it could be reasonable to type this case as `readonly["Apple", "Orange", "Pear"]` instead of `readonly string[]`. – Ben Regenspan Dec 09 '19 at 16:20
  • This way doesn't allow to use `fruits.include(var)` for a type check: `Argument of type 'string' is not assignable to parameter of type '"Apple" | "Orange" | "Pear"'.ts(2345)` – TotalAMD Jul 16 '20 at 11:13
  • @TotalAMD, It does if the variable is properly typed or casted. See https://tsplay.dev/GNl8lN – SgtPooki Oct 16 '20 at 04:54
  • 1
    why `fruits[number]` what does the number here mean? – Batman Dec 06 '20 at 22:13
  • 2
    @Batman writing `typeof fruits[number]` tells Typescript that what we are interested in is the type of the values stored within the `fruits` array. Because it's an Array, those values are indexed by `number`. In plain English, it's like we are asking TypeScript "for any given integer index requested from `fruits`, what are the possible types of the value that will be returned?" – Ben Regenspan Dec 08 '20 at 18:41
  • why not this instead ? : ```type fruits = "Apple" | "Orange" | "Pear";``` – aestheticsData Jan 04 '22 at 15:17
  • 1
    @aestheticsData the idea here is to--without the need for repeating the list of elements--define both 1) a JavaScript array that is accessible at runtime 2) a matching type. If you define the type manually, you need to repeat the list of items like this: `type fruits = "Apple" | "Orange" | "Pear"; const fruits: fruits[] = ["Apple", "Orange", "Pear"];` (Not necessarily always the worst thing, sometimes being DRY is overrated!) – Ben Regenspan Jan 04 '22 at 16:12
  • Bear in mind that `fruits` becomes readonly, so it will not be of type `Fruits[]`, and trying to use it as such results in an error like `The type 'readonly ["Apple", "Orange", "Pear"]' is 'readonly' and cannot be assigned to the mutable type '("Apple" | "Orange" | "Pear")[]'.`. This reduces the situations where this array can actually be used so I'm not sure how useful a solution it is. If there's a nice way to get both a union type, and an array of that type, without duplication I'd love to see it – Peter Cole Mar 29 '23 at 15:00
28

It can be done but first you need an extra function to help infer the string literal type for the array elements. By default Typescript will infer string[] for an array even if it is a constant. After we have an array of string literal types we can just use a type query to get the desired type

function stringLiteralArray<T extends string>(a: T[]) {
    return a;
}

const fruits = stringLiteralArray(["Apple", "Orange", "Pear"]);
type Fruits = typeof fruits[number]

Since 3.4 you can also use a const type assertion instead of the stringLiteralArray function:

const fruits = ["Apple", "Orange", "Pear"] as const;
type Fruits = typeof fruits[number]
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • 2
    This specific callout for `typeof fruits[number]` was what I needed - it makes it a string union instead of a readonly string[] – radicand Jun 19 '20 at 18:55