Lookup and mapped types

The keyof is a keyword in TypeScript that creates a union type of all the properties in an object. The type that is created is called a lookup type.  This allows us to create types dynamically, based on the properties of an existing type. It's a useful feature that we can use to create generic but strongly typed code against varying data. 

Let's go through an example:

  1. We have the following IPerson interface: 
interface IPerson {
id: number;
name: string;
}
  1. Let's create a lookup type on this interface using keyof:
type PersonProps = keyof IPerson;

If we hover over the PersonProps type, we see that a union type containing "id" and "name" has been created:

  1. Let's add a new property to IPerson:
interface IPerson {
id: number;
name: string;
age: number
}

If we hover over the PersonProps type again, we see that the type has been automatically extended to include "age":

So, the PersonProps type is a lookup type because it looks up the literals it needs to contain.

Let's create something useful now with a lookup type:

  1. We're going to create a Field class that contains the field name, a label, and a default value:
class Field {
name: string;
label: string;
defaultValue: any;
}
  1. This is a start, but we can make name more strongly typed by making our class generic: 
class Field<T, K extends keyof T> {
name: K;
label: string;
defaultValue: any;
}

We have created two generic parameters on the class. The first one is for the type of the object containing the field, and the second one is for the property name within the object. 

  1. It will probably make more sense if we create an instance of the class. Let's do just that using IPerson from the last example and passing "id" in as the field name:
const idField: Field<IPerson, "id"> = new Field();
  1. Let's try and reference a property that doesn't exist in IPerson:
const addressField: Field<IPerson, "address"> = new Field();

We get a compilation error, as we would expect:

Catching problems like this is the benefit of the lookup type, rather than using a string type. 

  1. Let's move our attention to the defaultValue property in our Field class. This is not type-safe at the moment. For example, we can set idField to a string:
idField.defaultValue = "2";
  1. Let's resolve this and make defaultValue type-safe:
class Field<T, K extends keyof T> {
name: K;
label: string;
defaultValue: T[K];
}

We are looking up the type using T[K]. For idField, this will resolve to the type of the id property in IPerson, which is number.

The line of code that sets idField.defaultValue now throws a compilation error, as we would expect:

  1. Let's change "2" to 2:
idField.defaultValue = 2;

The compilation error disappears.

So, lookup types can be useful when creating generic components for variable data types.

Let's move on to mapped types now. Again, these let us create new types from an existing type's properties. However, mapped types allow us to specifically define the properties in the new type by mapping them from the existing property.

Let's go through an example:

  1. First, let's create a type that we will map from in the next step:
interface IPerson {
id: number;
name: string;
}
  1. Now let's create a new version of the interface where all the properties are readonly using mapped type:
type ReadonlyPerson = { readonly [P in keyof IPerson]: IPerson[P] };

The important bit that creates the map is [P in keyof IPerson]. This iterates through all the properties in IPerson and assigns each one to P to create the type. So, the type that is generated in the previous example is the following:

type ReadonlyPerson = { 
readonly id: number
readonly name: string
};
  1. Let try this out to see if our type really is readonly:
let billy: ReadonlyPerson = {
id: 1,
name: "Billy"
};
billy.name = "Sally";

As we expect, a compilation error is thrown where we try to set the readonly property to a new value:

So our mapped type worked! A more generic version of this mapped type is actually in TypeScript as a standard type, and is Readonly<T>

  1. Let's use the standard readonly type now:
let sally: Readonly<IPerson> = {
id: 1,
name: "sally"
};
  1. Let's try changing the values in our readonly:
Sally.name = "Billy";

A compilation error is thrown, as we would expect:

If we were in Visual Studio Code and used the Go to Definition option on the Readonly type, we would get the following:

type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

This is very similar to our ReadonlyPerson type, but IPerson has been substituted with generic type T.

Let's have a go at creating our own generic mapped type now:

  1. We are going to create a mapped type that makes all the properties of an existing type of type string:
type Stringify<T> = { [P in keyof T]: string };
  1. Let's try to consume our mapped type:
let tim: Stringify<IPerson> = {
id: "1",
name: "Tim"
};
  1. Let's try to set id to a number:
tim.id = 1

The expected compilation error is thrown:

So, mapped types are useful in situations when we need a new type that is based on an existing type. Along with Readonly<T>, there are quite a few standard mapped types in TypeScript, such as Partial<T>, which creates a mapped type making all the properties optional.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.116.20