120

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?

3
  • 2
    Do you know the exact strings at compile time? If not, you cannot define such a type. Commented Aug 29, 2018 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"]; Commented Aug 29, 2018 at 20:26
  • stackoverflow.com/questions/45251664/… Commented Jan 18, 2020 at 21:38

2 Answers 2

217

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.

Sign up to request clarification or add additional context in comments.

11 Comments

why doesn't this work?: const fruitTypes = ["Apple", "Orange", "Pear"]; const fruits = fruitTypes as const;
@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.
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 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[].
@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?"
|
30

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]

2 Comments

This specific callout for typeof fruits[number] was what I needed - it makes it a string union instead of a readonly string[]
for anyone reading this, you literally put the word number in, not just substitute it with e.g. 0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.