As you know, TypeScript has a keyof
type operator that you can use to create a type out of all of the keys of an object:
type Animal = {
name: string;
age: number;
};
type Keys = keyof Animal; // "name" | "age"
(If you didn't know that, now you do).
But there is no corresponding valueof
operator. So how do you create a type out of all the values? Here's a concrete use case:
function getValue(animal: Animal, key: keyof Animal): any {
return animal[key];
}
How would you write the return type for this function?
Like this:
type Keys = keyof Animal; // "name" | "age"
type Values = Animal[Keys]; // string | number
You can stop reading here, or read on to understand how this works.
The first thing you need to know about is indexed access types. First a few types for our example.
type Track = {
title: string;
duration: number;
};
type Album = {
artist: string;
title: string;
tracks: Track[];
};
Now here's an indexed access type:
type Foo = Album["artist"]; // string
Foo
is now a type. It's a type alias for string
. This has a nice symmetry with using values:
// some example data
const kidA: Album = {
artist: "Radiohead",
title: "Kid A",
tracks: [
{ title: "Everything in Its Right Place", duration: 270 },
{ title: "Kid A", duration: 297 },
],
};
// indexing into an object
const artist = kidA["artist"]; // "Radiohead"
type ArtistType = Album["artist"]; // string
When you index into an object with a key, you get the related value. When you index into an object type, with a key type, you get the type of its related value. That's a lot of typing!
The indexing type you're passing into Album
is, well, a type. I know it looks like I'm passing in the string "artist"
. But "artist"
is a type in TypeScript. It's a subset of string
, just like you can use true
as a type instead of boolean
.
Artist
takes an indexing type. So you could pass in a different type!
Here's something you can't do in JavaScript:
type Bar = Album["artist" | "tracks"]; // string | Track[]
Two keys!!
Now, if it doesn't make sense why Bar
is string | Track[]
, think of it this way. What's the return type of this function?
function getArtistOrTracks(album: Album, key: "artist" | "tracks"): <sometype> {
return album[key];
}
If key is artist
, this returns a string
. If key is tracks
, this returns Track[]
. So if you said string | Track[]
, you are correct.
But we can take this one step further. We passed in two keys here. But remember, keyof
gives you a union of all the keys.
type Values = Album[keyof Album]; // string | number | Track[]
And we're back where we started. Using keyof Album
as the indexing type, we get a union type of all the values of Album
.
TypeScript won't let you use an invalid key for indexed access:
type Ugh = Album["ugh"]; // Error: Property 'ugh' does not exist on type 'Album'
Which is nice.
But annoyingly, you can't use string
either:
type Ugh = Album[string]; // Error: Type 'Album' has no matching index signature for type 'string'.ts(2537)
You might have thought, (I thought), since all keys are strings, Album[string]
would give me the same result as Album[keyof Album]
. Nope!
The error says "no matching index signature". If you don't know what index signatures are, read this. Basically, TypeScript wants me to change the album type to this:
type Album = {
artist: string;
title: string;
tracks: Track[];
[key: string]: any; // this is the index signature
};
Now Album[string]
doesn't give an error. But it's not very useful:
type Ugh = Album[string]; // any
You can use number
to get the type of an array's elements.
const data = [1, "two", true];
type TypeOfData = (typeof data)[number]; // string | number | boolean
Note you can't do:
type TypeOfData = data[number]; // 'data' refers to a value, but is being used as a type here.
You need typeof
.
But just typeof
isn't enough!
type TypeOfData = typeof data; // (string | number | boolean)[]
Because that gives you the type of the entire array, and you just want to know what type its elements could be.
valueof
type operator in TypeScript.Album[string]
.Album[keyof Album]
.