keyof, valueof

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.

How it 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!

Now it gets weird

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.

Errors

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

One last weird thing

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.

Conclusion

  1. There's no valueof type operator in TypeScript.
  2. I can't use Album[string].
  3. But I sure can use Album[keyof Album].
  4. Kid A is a great album.