Generic types in a nutshell
In the previous lessons, we learned about the basic data types including any
and unknown
. The any
type is a special data type in TypeScript as it represents all the values, hence it is also called a top type. We also learned about the union type which is used to create a type that represents a value whose type belongs to the union.
Let’s create a simple function that takes precisely two arguments of the same type and returns a tuple. Basically, this function wraps the argument values in an array. The argument value can be either string
or number
.
In the above example, we have defined the getTuple
function that takes arguments a
and b
of type NS
and returns a tuple of type [NS, NS]
. The NS
is just a type alias to the union type of string | number
.
💡 If you want to know more about
tuple
type, follow this lesson.
Now there are few problems with this function. First, we won’t be able to enforce that both a
and b
should have the same type since a
and b
can be either string
or number
irrespective of each other.
The second problem is that the return tuple (array) contains either string
or number
values in it and the TypeScript compiler won’t allow string
or number
specific operations since it can’t precisely tell the type of the values.
One way to get around this is use any
type for everything. Use it for arguments a
and b
as well as for the return tuple [any, any]
. Since any
is a top type and TypeScript allows all kinds of operations on it, you won’t get the compilation error.
Else, you can use type assertion to convert the type of a value in the tuple from NS
to string
or number
before calling type-specific methods. But in both cases, you can run into serious errors at runtime if you are not manually checking the type of these values.
This is where generics come in. A generic type is a parameter that can store a type (also sometimes called a type variable) and other type annotations can use it to refer to that type. This is just like how a variable refers to a value and we can use that variable where that value is needed, but generics only exist in the type system.
TypeScript provides rich support for generics. We can use generics with functions, classes, interfaces, and other types. Let’s modify the previous example and make this function generic.
Let’s talk about the signature of the getTuple
function first.
function getTuple<T>(a: T, b: T): [T, T] {
return [a, b];
}
The <T>
syntax makes the getTuple
function generic and it should come before the function parenthesis (()
). The T
represents the generic type parameter (like a variable) and it will contain the type passed in the function invocation syntax such as getTuple<string>
in which case T
is string
.
This generic type can be used across the function definition such as inside parameter declaration syntax, return type declaration as well as function body where the provided type needed to reference.
Hence when we invoke getTuple<number>
function, the TypeScript compiler replaces the T
with number
for that particular information. So technically, what the TypeScript compiler interprets the getTuple
function is as below.
function getTuple(a: number, b: number): [number, number] {
return [a, b];
}
Hence the return value of the function invocation at compile-time will be [number, number]
and the TypeScript compiler will allow number
related operation on the values of this tuple.
You are allowed to choose any generic parameter name, but a single letter such as T
in our case is preferred. When we call a generic function, we need to provide the type of this parameter using f<Type>()
syntax, however, that is optional in most cases.
If you look at the function signature of getTuple
function, the first use of T
is implemented for the first argument a
. Now you can guess that if the getTuple
function is called with the first argument value of type string
, then T
must be a string
.
Now that the TypeScript compiler understood what T
is in the function call, the second argument must be a string
and the return value must be [string, string]
. This makes the invocation of a generic function with type parameter values redundant.
In the above function call, we have dropped the value of the generic parameter. So the TypeScript will infer the value for T
from the type of the first argument 1.25
which is number
. Since the second argument must have the same type, this invocation will result in a compilation error since both arguments should be of the same type as per the function declaration.
💡 If you wanna know why the type of the second argument shows as
'world'
when it is clearly astring
, read the literal types section from the Type System lesson.
You can also make a function expression or an arrow function generic as shown below. As you can see, the <T>
is placed before the parenthesis.
var getTuple = <T>( a: T, b: T ): [ T, T ] => { ... }
Generic Type Declaration
TypeScript gives us the power to compose complex data types such as functions, interfaces, classes, etc. using generics.
Generic Functions
As we know, TypeScript can infer the type from the value. If you hover on the getTuple
function name in your IDE, you will see the below type signature of the function. This is the type of the function we just created.
var getTuple: <T>(a: T, b: T) => [T, T];
So the TypeScript compiler is creating a generic function type by looking at the generic parameter’s location in the function parameters and return value. But we can also explicitly provide a generic type and avoid annotating function parameters and return value.
A generic type can contain more than one parameter to represent different types. For example, if the function parameter a
and b
could have different types, then we need two different parameters to exclusively represent them.
In the above example, we have provided a generic function type explicitly for getTuple
variable. We have used two parameters to represent each function parameter instead of just one. Now the function arguments are no longer linked and they can attain any type.
var getTuple: <T, U>(a: T, b: U) => [T, U];
With this, the getTuple( 1.25, 'world' )
function invocation does not throw any errors anymore since argument a
and b
can have separate types.
You can also create a type alias to present a generic function type.
type TupleFunc = <T, U>( a: T, b: U ) => [ T, U ];
var getTuple: TupleFunc = ...;
Generic Interfaces
In the Interfaces lesson, we learned that an interface can also be used to represent a function. An interface with an anonymous function signature (and optional properties) declares a function type.
interface MyFunction {
(a: number, b: string): any;
}
The above interface type represents a function that takes two arguments of type number
and string
respectively and return a value of type any
. We can use this interface to annotate a value that is a function expression.
let myFunction: MyFunction = (a, b) => c;
In the above example, the argument a
and b
will get number
and string
type respectively and c
return value will get the any
type.
As we learned that a function can be made generic by annotating it with a generic function-type, we can implement the generic syntax inside an interface that represents a function.
The above example is the same as the previous example, the only difference is that we have created an interface that provides the generic function type.
Interfaces are normally used to represent objects of definite shapes. Let’s imagine if an object has fields a
and b
and it also has the getTuple
property which is a function that returns the tuple of these field values. How would you write that interface?
interface TupleObject {
a: T; // ← invalid
b: U; // ← invalid
getTuple<T, U>(): [T, U];
}
Sadly, this example doesn’t work because the TypeScript would consider T
and U
as normal types for the fields a
and b
and they don’t exist in our example. And another thing is T
and U
of the getTuple
function are not the same as T
and U
of a
and b
fields (if this syntax would’ve been valid).
Fortunately, an entire interface can be made generic. The members of the interface can derive the type information from the interface. This way, multiple members can be linked together to share the same types.
interface TupleObject<T, U> {
a: T;
b: U;
getTuple(): [T, U];
}
In the above example, since the interface itself is generic, TypeScript can use these generic parameters to provide the type information for the rest of the members of the interface. Here, the T
and U
in all the interface members refers to the same value, hence if T
is string
then a
is string
and so is the first element of the returned tuple.
Let’s consider another scenario for the above example. What if the getTuple
function accepts a generic argument? Well, in that case, we can add another generic parameter in the TupleObject
interface and make use of that.
interface TupleObject<T, U, V> {
a: T;
b: U;
getTuple(c: V): [T, U, V];
}
However, the TupleObject
interface defines an object with the fixed type of argument c
. Hence whenever you define an object of type TupleObject
, you will provide the type for the argument c
as well and it will be fixed until the end of days. So what will happen when we provide a wrong argument value?
var tupleObj: TupleObject<number, number, string> = {
a: 1, b: 2, getTuple: function( c ) {
return [ this.a, this.b, c ];
}
};
// Error: Argument of type '1' is not assignable to parameter of type 'string'
var tuple1 = tupleObj1.getTuple( 1 );
In the above example, since the type of argument c
of the getTuple
function of the tupleObj
is set to string
, you won’t be able to call the function with any argument value other than string
. But if you want getTuple
to accept the type provided in the function call, we can make the function generic.
In the above example, the interface TupleObject
is generic but so is the getTuple
function inside it. The generic type T
and U
for the return value of this function is derived from the interface while the V
is extracted from the function invocation.
When we call tupleObj.getTuple()
function, we need to provide the value of the generic parameter V
which will be used by the TypeScript compiler to evaluate the final type of the return value. However, we don’t need to explicitly provide it since TypeScript can find out about V
from the type of the argument value passed to tupleObj.getTuple()
call.
Implementing Generic Interfaces
In the Classes lesson, we learn how we can enforce constraints on a class definition by implementing an interface using the implements
keyword.
interface PersonInterface { ... }
class Person implements PersonInterface { ... }
When a class implements an interface, a class needs to define all public members provided by the interface with exactly the same type. This include public properties and methods of the class.
Let’s imagine a scenario where you want to define an interface that can be implemented by multiple classes. This is a fairly general use case. However, some members of this interface can have a specific type tailored for a specific class implementing it. Let’s see a small example.
interface Secret {
secret: string | number;
}
In the Secret
interface, the secret
property can hold string
or number
value. This interface can be implemented by multiple classes but specific classes implement either string
or number
. This is where the problem starts since this interface can not enforce that constraint. It can only enforce that the class must have secret
property and it must be either string
or number
.
class Student implements Secret {
public secret: string; // bad type
constructor(value: string) {
this.secret = value;
}
}
In the above example, class Student
implements the Secret
interface but there is an issue. This class was supposed to store secret
as a number
but since string
is also allowed by the interface, there was no error.
This is where a generic interface can help us.
In the above example, the type for the secret
property of the Secret
interface is inferred from the generic parameter T
. When the Student
class implements this interface, it needs to provide the value of T
. Since T
here is number
, the type of secret
property must be number
as enforced by the interface.
💡 A generic interface can have methods as well which can enforce the parameter types and return value type constraints on the class.
Generic Classes
In the previous example, we saw how a generic interface can shape the implementation of a class. This, however, does not produce a generic class since the value of secret
property is fixed with the class after the class is declared.
As we learned in the Classes lesson, a class defines an implicit interface that contains public members of the class. An instance of the class created using new
keyword has this interface type, hence it is also called the instance side type of the class. Let’s see a quick example.
class Student {
constructor(
public name: string,
public marks: number,
) {}
}
var ross = new Student("Ross Geller", 84);
ross.name; // string
ross.marks; // number
In the above example, ross
has the type of Student
which is an implicit interface defined by the Student
class that has name
field of type string
and marks
field of type number
. Any and all instances of Student
class will have this exact same type, no matter how it was created.
But, imagine a situation where the properties or methods of a class needs to have a different type-signature based on the instance itself. We can certainly use any
or a union type to mitigate this situation. Let’s see an example.
class Collection {
public items: (string | number)[];
}
Here, the Collection
class has the items
property that holds an array of string
or number
items. When you create an instance of Collection
class, you won’t be able to perform string
or number
related operations on values of items
array since its type is string | number
.
Fortunately, TypeScript supports generic classes. Just like a generic interface, a class can have a generic signature that provides types to its members.
In the above example, Collection
is a generic class since the generic parameter T
provides types to the public members, constructor parameters, and class methods. When an instance of a generic has to be created using new
keyword, we provide the value for this T
using new Collection<Type>
syntax.
As TypeScript compiler replaces the T
with the actual type provided during the instantiation of the class, only those members will be able to infer this generic type which are invoked during this process. These are you public
, private
and protected
properties and methods as well as the constructor function of the class.
All static
members live on the class itself, hence they will be initialized only once when the class is defined. Due to this reason, they won’t be able to infer the generic types passed during the instantiation of the class, and TypeScript won’t allow you to use the T
for static properties or method definitions.
You can pass the value of a generic parameter from generic subclass to a generic superclass in inheritance as demonstrated below. I have used parameter U
just to demonstrate that the parameter name doesn’t matter across classes and the same applies to the interfaces.
Generic Factory Functions
In the Classes lesson, we also learned about the distinct static vs instance side of the class. When we refer to a class as a type, for example :Person
, we are referring to the implicit interface type produced by the class that contains all of its public members. Every instance of this class gets this type implicitly.
However, when you want to refer to the type of the class itself, you can use typeof Person
expression which returns an interface that contains static side members of the class such as constructor function and all static
fields. You should learn more about this from the Classes lesson, it’s very interesting.
Let’s create a simple factory function that accepts any class as an argument and returns an instance of that class. We need to make this function generic in some way to make that happen.
function simpleFactory<T>(ctor: new () => T): T {
return new ctor();
}
The simpleFactory
function accepts a ctor
argument value which should be a type of constructor function. Since the only necessary condition for a value of this type is a constructor function, any class qualifies for this.
In the above example, the simpleFactory
is a generic function that accepts exactly one generic type parameter T
. The ctor
argument must be of the Ctor<T>
which is a constructor function type that returns a value of type T
.
💡 Here
Ctor
is a generic type alias. You can also use a generic interface to represent a constructor function as described in the Classes lesson.
Generic Constraints
We know that extends
keyword is a valid JavaScript keyword that is used to extend the functionality of a class from another class. In layman’s terms, A extends B
imports all the public (and non-static) members of the class B
and puts it inside class A
, therefore class A
inherits class B
.
But the extends
keyword can be used in a type annotation as well, but its behavior is very different. This keyword is generally used in a generic type to produce a compile-time constraint.
💡 This might remind you of the
typeof
keyword. It is a valid keyword in JavaScript but when it is used in a type annotation such astypeof val
, it returns the type of variableval
.
Let’s first understand how this keyword can be used.
<T extends MyType, U, V, ...>
So far we have used single letters as parameters to define a generic type. The TypeScript compiler replaces these parameters based on the user input type at the compile-time and evaluates the final type of the function
or class
or any other generic type to which it belongs.
In the above example, the generic parameters U
and V
are familiar but T exends MyType
is a new thing. What this means is that whatever the value of T
is, it should extend the MyType
. So any type that extends MyType
will be able to pass as T
and the TypeScript compiler won’t complain.
For example, if the Type
is a Person
class then T
must be a class that inherits Person
class. Hence you will be able to pass Teacher
or Student
class as T
in this generic syntax if these classes extend Person
class.
You can apply the same for the interfaces. In the Type System lesson, we talked about duck typing or structural subtyping. In TypeScript, type A
is a valid type B
if type A
has all the properties of type B
.
class Person {
constructor(
public firstName: string,
public lastName: string,
) {}
}
class Student {
constructor(
public firstName: string,
public lastName: string,
public marks: number,
) {}
}
function getFullName(p: Person): string {
return `${p.firstName} ${p.lastName}`;
}
var ross = new Person("Ross", "Geller");
var monica = new Student("Monica", "Geller", 84);
console.log("Ross =>", getFullName(ross));
console.log("Monica =>", getFullName(monica));
In the above example, even though the class Student
doesn’t extends Person
class but has all the properties of it, then it would be a valid Person
at compile-time. So a function that accepts an argument of type Person
(an instance of Person
) would also accept an instance of Student
.
💡 You can also test this example with objects literals or interfaces instead of the instance of the classes and it will still give the same results.
The extends
keyword in generic type follows the same logic. If the value for the generic parameter T
has all the properties of Type
then T extends Type
is valid and the TypeScript compiler will gladly accept the value of T
.
Let’s modify the previous example and make getFullName
method generic with a constraint. The getFullName
should only accept an argument value if that value extends the type of Person
interface.
As you can see, the Student
type is a subtype of Person
type, therefore Student extends Person
and Person extends Person
would evaluate to be legal in the generic constraint of getFullName
method. We do not need to deliberately provide the value for the parameter T
such as in getFullName<Student>
as it would be inferred from the argument p
.
As the structural typing goes, TypeScript only looks at the shape of the value. Hence even though the rachel
object hasn’t been provided a specific type (it infers from the object literal value), it extends Person
type since it has all the properties of Person
, hence (typeof rachel) extends Person
is valid.
However, the TypeScript compiler doesn’t compile this program because of the last statement. The joey
object is a type of Teacher
which is not a subset of Person
, hence Teacher extends Person
is invalid.
💡 You can also use
typeof obj
in the generic type to extract the type from a value. For example,<T extends typeof ross>
would behave be similar toT extends Person
or<T extends { firstName: string, lastName: string }>
.
Generics Constraints using Union Types
In the Type System lesson, we learned about union types. A union is a set of types and a value whose type is included in this set can be represented as the union type.
var str = "hello";
var num = 123;
var bool = true;
function func(arg: string | number) {}
func(str); // ✅ valid
func(str as string); // ✅ valid
func(num); // ✅ valid
func(num as number); // ✅ valid
// Error: Argument of type 'true' is not assignable to parameter of type 'string | number'.
func(bool); // ❌ invalid
// Error: Argument of type 'boolean' is not assignable to parameter of type 'string | number'.
func(bool as boolean); // ❌ invalid
As we can see from the example above, string | number
can represent only those values whose type is either string
or number
. Even if a type is a subset of one of these types, for example str
in the above example whose type is 'hello'
(which a subset of string
), it is a valid string | number
value.
💡
'hello'
is also a subtype of itself, so if the union has'hello'
type, it would also qualify. You can learn more about the literal types or the unit types such as'hello'
used in the above example from the Type System lesson.
So technically speaking, a type that is a subtype of any of the types in a union can be said to extend the union type. That means we should be able to use a union type in the as a generic constraint.
var prettify = <T extends string | number>(value: T) => {};
prettify("Hello"); // ✅ valid
prettify("Hello" as string); // ✅ valid
prettify(123); // ✅ valid
// Error: Argument of type 'true' is not assignable to parameter of type 'string | number'.
prettify(true); // ❌ invalid
In the above example, we learned in the generic type expressionT extends U
, if the U
gets a union type then type T
must be in the union set or should be a subset of one of the types in the union. For example, if T
is 'hello'
which is a subset of 'hello'
literal type as well as the string
collective type, U
must contain the 'hello'
type in the set ('hello' | number | ...
) or the string
type (string | number | ...
).
Now imagine if T
is also a union type. What would happen then? Well, if T
is a union type, then every type in the T
is matched against the U
union type. If all the types in the T
satisfy Tᵢ extends U
then the value for T
is valid.
In the above example, the getValue
generic function accepts the value for T
if it extends string | number | boolean
union type. The value for T
is inferred from the type of arg
argument value. The return value of this function also has the type of T
.
The type for value1
and value2
is kind of obvious since the type 'hello'
and the type string
is a subset of string
hence they qualify for the value of T
(as the string
type exists in the union).
When value for T
is string | number
, it is also valid since the string
and the number
type individually exists in the union. The same logic can be applied to any
since it can represent all types in the union.
However, since the symbol
type is not in the set, symbol
or a union type that contains symbol
is invalid.
Type Parameters in Generic Constraints
One of the wonderful applications of using union type in this fashion is with the keyof
operator. The keyof
operator, when placed before an interface, returns the field names of that interface as a union of literal types. Each literal type in this union is a field name (or key) of that interface.
In the above example, K extends keyof V
means that K
should extend the union of the key names of V
. Since the value for V
is derived from the type of argument obj
, for the first two getValue()
calls, V
is car
. Hence keyof V
is keyof (typeof car)
which in turn is 'name' | 'model'
.
Also, the value for K
is derived from the literal type of key
argument value. Therefore 'name' extends 'name' | 'model'
is valid when the key
is 'name'
. The same goes for the 'model'
argument value.
For the fruit
object, keyof V
yields 'type' | 'color' | 'weight'
. The getValue( fruit, 'color' as string )
expression is not valid as string
type is not in this set. Similarly, the getValue( fruit, 'name')
call is invalid since the 'name'
type is not in this set.
Conditional Types
The T extends U
expression is not only useful for the TypeScript compiler to allow the value for the parameter T
if it extends the type U
but we can also use it to conditionally do something. The T extends U
expression evaluates to either true
or false
at the compile-time, so let’s exploit that.
You must be familiar with the ternary expression in JavaScript. Well, we can use that in type annotation syntax as well.
Type1 extends Type2 ? Type3 : Type4
In the above example, Type1
, Type2
, Type3
and Type4
are normal types like string
, interface
and whatnot, even generic type parameters. This expression returns Type3
when Type1
extends Type2
, else it returns Type4
.
We can use this expression to return a type based on whether Type1
extends Type2
or not. This can be helpful to annotate other values. But keep in mind, this a compile-time feature, and this expression only works in type annotation or type alias as shown below.
// type MyType1 = string
type MyType1 = string extends any ? string : never;
// type MyType2 = string
type MyType2 = "hello" extends string | number ? string : never;
// var value: never
var value: string extends number ? string : never;
In the above example, the MyType1
type alias receives the type string
since string extends any
evaluates to true
. Similarly, since the literal type 'hello'
extends the string
type in the union, string extends (string | number)
evaluates to true
and string
is returned from the expression.
However, the type of value
is never
, since string extends number
evaluates to false
, hence the never
type is returned from the expression.
Since a type can be evaluated conditionally using the ternary expression, a type alias can also be represented as a generic type.
type MyType<T> = T extends string ? string : never;
// type MyType1 = string
type MyType1 = MyType<string>;
// type MyType2 = string
type MyType2 = MyType<"hello">;
// type MyType3 = never
type MyType3 = MyType<number>;
In the above example, we have made MyType
type alias generic and it can accept a value for the type parameter T
. This value is used in the ternary expression to evaluate a concrete type.
The MyType<string>
expression is equivalent to string extends string ? string : never
since the value for T
is passed as string
. Therefore the value received by Type1
is string
. The same principle applies to the Type2
and Type3
. With this, you can create customized types based on your needs.
Let’s modify the previous example and return T
from the ternary expression.
type MyType<T> = T extends string ? T : never;
// type MyType1 = string
type MyType1 = MyType<string>;
// type MyType2 = 'hello'
type MyType2 = MyType<"hello">;
// type MyType3 = never
type MyType3 = MyType<number>;
The only modification we made in this example is instead of returning string
from the ternary expression when T extends string
is true
, we are returning the incoming type (_value of _T
) itself back.
Due to this change, the MyType2
receives the type 'hello'
which was the type passed for the placeholder T
. Here, MyType<T>
generic type is actually acting like a filter by ignoring all the values that do not extend string
type.
As we learned, when T
and U
in the T extends U
expression are union types, each type (item) in the T
is evaluated for the extends U
criteria. When that’s the case, the ternary expression is distributed over each value in T
.
For example, if A | B
are the types in the union T
then the T extends U ? X : Y
will be distributed as (A extends U ? X : Y) | (B extends U ? X : Y)
. Due to this behavior, a generic conditional type is also called a distributed conditional type. Let’s see an example of this.
type MyType<T> = T extends string ? string : number;
// type MyType1 = string | number
type MyType1 = MyType<"hello" | "world" | 1 | 2>;
type MyType<T> = T extends string ? string : number; // type MyType1 = string | number\
type MyType1 = MyType<"hello" | "world" | 1 | 2>;
In the above example, the MyType1
receives the string | number
type since for the 'hello'
and 'world'
types, the ternary expression returns string
and number
for the 1
and 2
types. So in the end, a union of string | string | number | number
is coerced into string | number
by the TypeScript.
This example would become a lot interesting if we return never
from the ternary expression. Why? Because never
does not represent any values, hence TypeScript will drop the never
type in a union.
type MyType<T> = T extends string ? string : never;
// type MyType1 = string
type MyType1 = MyType<"hello" | "world" | 1 | 2>;
In the above example, MyType1
gets the string
type because the union of string | string | never | never
is coerced into just string
.
TypeScript provides built-in generic types that are used to filter types based on certain criteria. They are explained in the Utility Types lesson. Let’s recreate the NonNullable
conditional type provided by the TypeScript.
type Safe<T> = T extends null | undefined ? never : T;
// type MyType1 = string | number | boolean
type MyType1 = Safe<string | undefined | number | boolean | null>;
Here, the Safe
type behaves exactly like the NonNullable
conditional type provided by the TypeScript. All the values which extend to null
or undefined
will be returned as never
from the ternary expression, otherwise, the original type is returned as in string | never | number | boolean | never
. In the end, we are getting a union without null
or undefined
types.
You must be thinking, “Why would I ever need a conditional type or constrained generic types in general? Anybody who needs a conditional type to extract what non-nullable
types shouldn’t be programming in TypeScript”.
Well, you are right but let’s take an example where you have an interface A
with multiple properties but you want an interface type B
that has only a few properties of A
. You can create it manually but using a conditional type is much easier. TypeScript provides Pick
type for this purpose.
interface Student {
firstName: string;
lastName: string;
marks: number;
}
// type Person = { firstName: string; lastName: string;}
type Person = Pick<Student, "firstName" | "lastName">;
In the above example, using Pick
conditional type, we have created a new interface type Person
that has only the firstName
and lastName
properties of the Student
interface. Let’s see how the Pick
generic type is implemented.
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
The first type parameter T
is the interface and second type parameter K
should extend the union of the property names of the interface T
, which also means K
should be a string literal type or a union of string literal types that is a subset of string literal types created bykeyof T
.
keyof Student => 'firstName' | 'lastName' | 'marks'
('firstName' | 'lastName' | 'marks') extends keyof T => true
('firstName' | 'lastName') extends keyof T => true
('firstName') extends keyof T => true
('firstName' | 'age') extends keyof T => false
If we provide a property name in the union type K
which does not belong to the interfaceT
, then the TypeScript compiler will throw an error right away.
The [P in K]: T[P]
expression instructs the TypeScript compiler to iterate over the value of K
, kind of like for in
loop. In the interface lesson, we learned about this signature which is also called an index signature. The T[P]
expression picks the type of the field P
from the interface T
.
Using the same principles, you will be able to create a mapped type that creates a new interface by providing a new behavior to each property of the interface. TypeScript provides Readonly
utility type that returns an interface by making each property of the input interface readonly
.
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
💡 I have explained major utility types in the Utility Types lesson, but if you want to see how these utility types are implemented under the hood, follow the
es5.d.ts
file in the standard TypeScript installation or use this link which points to the same file in the official TypeScript GitHub repository.