Categories
JavaScript TypeScript

TypeScript Advanced Types — this Type and Dynamic Types

Spread the love

TypeScript has many advanced type capabilities, which makes writing dynamically typed code easy. It also facilitates the adoption of existing JavaScript code since it lets us keep the dynamic capabilities of JavaScript while using the type-checking capability of TypeScript. There are many kinds of advanced types in TypeScript, like intersection types, union types, type guards, nullable types, and type aliases, and more. In this article, we’ll look at the this type and creating dynamic types with index signatures and mapped types.

This Type

In TypeScript, we can use this as a type. It represents the subtype of the containing class or interface. We can use it to create fluent interfaces easily since we know that each method in the class will be returning the instance of a class.

For example, we can use it to define a class with chainable methods like in the following code:

class StringAdder {  
  value: string = '';  
  getValue(): string {  
    return this.value;  
  } 

  addFoo(): this {  
    this.value += 'foo';  
    return this;  
  } 

  addBar(): this {  
    this.value += 'bar';  
    return this;  
  } 

  addGreeting(name: string): this {  
    this.value += `Hi ${name}`;  
    return this;  
  }  
}
const stringAdder: StringAdder = new StringAdder();  
const str = stringAdder  
  .addFoo()  
  .addBar()  
  .addGreeting('Jane')  
  .getValue();  
console.log(str);

In the code above, the addFoo, addBar, and addGreeting methods all return the instance of the StringAdder class, which lets us chain more method calls of the instance to it once it’s instantiated. The chaining is made possible by the this return type that we have in each method.

Index Types

To make the TypeScript compiler check code with dynamic property names, we can use index types. We can use the extends keyof keyword combination to denote that the type has the property names of another type. For example, we can write:

function choose<U, K extends keyof U>(o: U, propNames: K[]): U[K][] {  
  return propNames.map(n => o[n]);  
}

Then we can use the choose function as in the following code:

function choose<U, K extends keyof U>(o: U, propNames: K[]): U[K][] {  
  return propNames.map(n => o[n]);  
}

const obj = {  
  a: 1,  
  b: 2,  
  c: 3  
}  
choose(obj, ['a', 'b'])

Then we get the values:

[1, 2]

if we log the results of the choose function. If we pass in a property name that doesn’t exist in the obj object into the array in the second of the choose function, then we get an error from the TypeScript compiler. So if we write something like the following code:

function choose<U, K extends keyof U>(o: U, propNames: K[]): U[K][] {  
  return propNames.map(n => o[n]);  
}const obj = {  
  a: 1,  
  b: 2,  
  c: 3  
}  
const arr = choose(obj, ['d']);

Then we get the error:

Type 'string' is not assignable to type '"a" | "b" | "c"'.(2322)

In the examples above, keyof U is the same as the string literal type “a” | “b” | “c” since we passed in the type of the generic U type marker where the actual type is inferred from the object that we pass in into the first argument. The K extends keyof U part means that the second argument must have an array of some or all the key names of whatever is passed into the first argument, which we denoted by the generic U type. Then we defined the return type as an array of values that we get by looping through the object we pass into the first argument, hence we have the U[K][] type. U[K][] is also called the index access operator.

Index types and Index Signatures

An index signature is a parameter that must be of type string or number in a TypeScript interface. We can use it to denote the properties of a dynamic object. For example, we can use it like we do in the following code:

interface DynamicObject<T> {  
  [key: string]: T;  
}  
let obj: DynamicObject<number> = {  
  foo: 1,  
  bar: 2  
};  
let key: keyof DynamicObject<number> = 'foo';  
let value: DynamicObject<number>['foo'] = obj[key];

In the code above, we defined a DynamicObject<T> interface which takes a dynamic type for its members. We have an index signature called the key which is a string. It can also be a number. The type of the dynamic members is denoted by T, which is a generic type marker. This means that we can pass in any data type into it.

Then we defined the obj object, which is of type DyanmicObject<number>. This makes use of the DynamicObject interface we created earlier. Then we defined the key variable, which has the type keyof DynamicObject<number>, which means that it has to be a string or a number. This means that the key variable must have one of the property names as the value. Then we defined the value variable, which must have the value of an object of type DynamicObject .

This means that we can’t assign anything other than a string or number to the key variable. So if write something like:

let key: keyof DynamicObject<number> = false;

Then we get the following error message from the TypeScript compiler:

Type 'false' is not assignable to type 'string | number'.(2322)

Mapped Types

We can create a new type by mapping the members of an existing type into the new type. This is called a mapped type.

We can create mapped types like we do in the following code:

interface Person {  
  name: string;  
  age: number;  
}

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

type PartialType<T> = {  
  [P in keyof T]?: T[P];  
}

type ReadOnlyPerson = ReadOnly<Person>;  
type PartialPerson = PartialType<Person>;let readOnlyPerson: ReadOnlyPerson = {  
  name: 'Jane',  
  age: 20  
}
readOnlyPerson.name = 'Joe';  
readOnlyPerson.age = 20;

In the code above, we created the ReadOnly type alias to let us map the members of an existing type into a new type by setting each member of the type as readonly. This isn’t a new type on its own since we need to pass in a type to the generic type marker T . Then we create an alias for the types that we defined by passing in the Person type into the ReadOnly alias and Partial alias respectively.

Next we defined a ReadOnlyPerson object with the name and age properties set. Then when we try to set the values again, then we get the following errors:

Cannot assign to 'name' because it is a read-only property.(2540)Cannot assign to 'age' because it is a read-only property.(2540)

Which means that the readonly property from the ReadOnly type alias is being enforced. Likewise, we can do the same with the PartialType type alias. We have defined the PartialPerson type by mapping the members of the Person type to the PartialPerson type with the PartialPerson type. Then we can define a PartialPerson object like in the following code:

let partialPerson: PartialPerson = {};

As we can see, we can omit properties from the partialPerson object we as want.

We can add new members to the mapped type alias by creating an intersection type from it. Since we used the type keyword to define the mapped types, they’re actually actually types. They are actually type aliases. This means that we can’t put members straight inside, even though they look like interfaces.

To add members, we can write something like the following:

interface Person {  
  name: string;  
  age: number;  
}

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

type ReadOnlyEmployee = ReadOnly<Person> & {  
  employeeCode: string;  
};

let readOnlyPerson: ReadOnlyEmployee = {  
  name: 'Jane',  
  age: 20,  
  employeeCode: '123'  
}

Readonly<T> and Partial<T> are included in the TypeScript standard library. Readonly maps the members of the type that we pass into the generic type placeholder into read-only members. The Partial keyword lets us map members of a type, into nullable members.

Conclusion

In TypeScript, we can use this as a type. It represents the subtype of the containing class or interface. We can use it to create fluent interfaces easily since we know that each method in the class will be returning the instance of a class. An index signature is a parameter that must be of type string or number in a TypeScript interface.

We can use it to denote the properties of a dynamic object. To convert members of a type to add some attributes to them, we can map the members of an existing type into the new type to add the attributes to the interface with mapped types.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *