Headshot of Marius Schulz
Marius Schulz Front End Engineer

TypeScript 2.1: keyof and Lookup Types

JavaScript is a highly dynamic language. It can be tricky sometimes to capture the semantics of certain operations in a static type system. Take a simple prop function, for instance:

function prop(obj, key) {
  return obj[key];
}

It accepts an object and a key and returns the value of the corresponding property. Different properties on an object can have totally different types, and we don't even know what obj looks like.

So how could we type this function in TypeScript? Here's a first attempt:

function prop(obj: {}, key: string) {
  return obj[key];
}

With these two type annotations in place, obj must be an object and key must be a string. We've now restricted the set of possible values for both parameters. The return type is still inferred to be any, however:

const todo = {
  id: 1,
  text: "Buy milk",
  due: new Date(2016, 11, 31)
};

const id = prop(todo, "id");     // any
const text = prop(todo, "text"); // any
const due = prop(todo, "due");   // any

Without further information, TypeScript can't know which value will be passed for the key parameter, so it can't infer a more specific return type for the prop function. We need to provide a little more type information to make that possible.

The keyof Operator

Enter TypeScript 2.1 and the new keyof operator. It queries the set of keys for a given type, which is why it's also called an index type query. Let's assume we have defined the following Todo interface:

interface Todo {
  id: number;
  text: string;
  due: Date;
}

We can apply the keyof operator to the Todo type to get back a type representing all its property keys, which is a union of string literal types:

type TodoKeys = keyof Todo; // "id" | "text" | "due"

We could've also written out the union type "id" | "text" | "due" manually instead of using keyof, but that would've been cumbersome, error-prone, and a nightmare to maintain. Also, it would've been a solution specific to the Todo type rather than a generic one.

Indexed Access Types

Equipped with keyof, we can now improve the type annotations of our prop function. We no longer want to accept arbitrary strings for the key parameter. Instead, we'll require that the key actually exists on the type of the object that is passed in:

function prop<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

TypeScript now infers the prop function to have a return type of T[K], a so-called indexed access type or lookup type. It represents the type of the property K of the type T. If we now access the three todo properties via the prop method, each one will have the correct type:

const todo = {
  id: 1,
  text: "Buy milk",
  due: new Date(2016, 11, 31)
};

const id = prop(todo, "id");     // number
const text = prop(todo, "text"); // string
const due = prop(todo, "due");   // Date

Now, what happens if we pass a key that doesn't exist on the todo object?

Invalid key

The compiler complains, and that's a good thing! It prevented us from trying to read a property that's not there.

For another real-world example, check out how the Object.entries() method is typed in the lib.es2017.object.d.ts type declaration file that ships with the TypeScript compiler:

interface ObjectConstructor {
  // ...
  entries<T extends { [key: string]: any }, K extends keyof T>(o: T): [keyof T, T[K]][];
  // ...
}

The entries method returns an array of tuples, each containing a property key and the corresponding value. There are plenty of square brackets involved in the return type, admittedly, but there's the type safety we've been looking for!

This post is part of the TypeScript Evolution series:

  1. TypeScript 2.0: Non-Nullable Types
  2. TypeScript 2.0: Control Flow Based Type Analysis
  3. TypeScript 2.0: Acquiring Type Declaration Files
  4. TypeScript 2.0: Read-Only Properties
  5. TypeScript 2.0: Tagged Union Types
  6. TypeScript 2.0: More Literal Types
  7. TypeScript 2.0: The never Type
  8. TypeScript 2.0: Built-In Type Declarations
  9. TypeScript 2.1: async/await for ES3/ES5
  10. TypeScript 2.1: External Helpers Library
  11. TypeScript 2.1: Object Rest and Spread
  12. TypeScript 2.1: keyof and Lookup Types
  13. TypeScript 2.1: Mapped Types
  14. TypeScript 2.1: Improved Inference for Literal Types
  15. TypeScript 2.1: Literal Type Widening
  16. TypeScript 2.1: Untyped Imports
  17. TypeScript 2.2: The object Type
  18. TypeScript 2.2: Dotted Properties and String Index Signatures
  19. TypeScript 2.2: Null-Checking for Expression Operands
  20. TypeScript 2.2: Mixin Classes
  21. TypeScript 2.3: Generic Parameter Defaults
  22. TypeScript 2.3: The --strict Compiler Option
  23. TypeScript 2.3: Type-Checking JavaScript Files with --checkJs
  24. TypeScript 2.3: Downlevel Iteration for ES3/ES5
  25. TypeScript 2.4: String Enums
  26. TypeScript 2.4: Weak Type Detection
  27. TypeScript 2.4: Spelling Correction
  28. TypeScript 2.4: Dynamic import() Expressions
  29. TypeScript 2.5: Optional catch Binding
  30. TypeScript 2.6: JSX Fragment Syntax
  31. TypeScript 2.7: Numeric Separators
  32. TypeScript 2.7: Strict Property Initialization
  33. TypeScript 2.8: Per-File JSX Factories
  34. TypeScript 2.8: Conditional Types
  35. TypeScript 2.8: Mapped Type Modifiers