Check out my books on Amazon at https://www.amazon.com/John-Au-Yeung/e/B08FT5NT62
Subscribe to my email list now at http://jauyeung.net/subscribe/
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> {
}
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.