Chapters - Table of contents →
Introduction
So far, we've learned to type basic variables and objects and, to build flexible and configurable functions with the help of Generics and overloads.
As we will see in Chapter 4 ("TypeScript Architecture"), it is important that your application avoid duplicating type definitions and favors building "transformed types" or "types of types".
See the example below:
1interface User {
2 id: string,
3 firstName: string,
4 lastName: string,
5}
6
7interface RegisterUserForm {
8 firstName?: string,
9 lastName?: string,
10}
11
A RegisterUserForm object, used in a <UserRegisterForm> form component waits for firstName and lastName to be filled, for this reason, firstName and lastName can be undefined.
However, we expect any User to have firstName and lastName filled.
This code duplication makes our types weaker because a future change to User might not be applied to RegisterUserForm.
Ideally, we would prefer to have RegisterUserForm built "on top of" the User type.
Luckily, TypeScript will provide us with all the tools to build "types of types".
Building a type from another type
A RegisterUserForm type built from the User type will look as it follows:
1type RegisterUserForm = Partial<Omit<User, 'id'>>
2
3// RegisterUserForm type is:
4// {
5// firstName?: string
6// lastName?: string
7// }
8
Let's analyze the snippet above:
The type keyword
This new TypeScript type keyword helps to define object types, similarly to interface.
However, an interface cannot be assigned to a type expression as follows:
1// The following line is invalid!
2interface RegisterUserForm = Partial<Omit<User, 'id'>>
3
Following this limitations, interface is useful to define root definitions (ex: User), functions and, extending other interfaces.
To create a type from another or multiple other types, we will use the type keyword.
When should I use interface or type? A rule of thumb 👍
__
You will need type to achieve the following:
-
Rename an existing type
type Name = string
-
Transform an existing type (changing properties type)
type RegisterUserForm = Partial<Omit<User, 'id'>>
-
Create a subset of a type
type UserWithId = Pick<User, 'id'>
-
Combine multiple types together
(More on this later)
__
➡️Otherwise, you can use the interface keyword.
The Partial<T> and Omit<T, K> type helpers
Let's come back to our RegisterUserForm type definition:
1type RegisterUserForm = Partial<Omit<User, 'id'>>
2
We can see that two generic types are called: Partial<T> and, Omit<T, K>.
Those types, also called type helpers, are provided by TypeScript (lib.es5.d.ts).
Let's see their definitions:
- Partial<T>: given a T type, return a new type with all T properties optional.
1interface User {
2 id: string
3 firstName: string
4 lastName: string
5}
6
7type UserWithOptionalProperties = Partial<User>
8// `UserWithOptionalProperties` type is
9// {
10// id?: string
11// firstName?: string
12// lastName?: string
13// }
14
- Omit<T, K>: given a T type and a K type, return a new type omitting the K properties of T
1interface User {
2 id: string
3 firstName: string
4 lastName: string
5}
6
7type UserWithoutLastName = Omit<User, 'lastName'>
8// `UserWithoutLastName` type is
9// {
10// id: string
11// firstName: string
12// }
13
By combining them as follows: Partial<Omit<User, 'id'>>, we define RegisterUserForm as:
"RegisterUserForm has all the properties of User - expect id - but optional".
Other TypeScript type helpers to keep in mind 💡
-
Pick<T, K>: given a T type and a K type, return a new type only including the K properties of T
Pick<{ a: string, b: number }, 'a'>gives{ a: string }
-
Required<T> is the opposite of Partial<T>
Required<{ a?: string, b?: number }>gives
{ a: string, b: number }
-
NonNullable<T> removes null and undefined from a givenTtype
NonNullable<string | null> gives string
-
ReturnType gives the return type of the provided function type
ReturnType<(a: number, b number) => number> gives number
Next
Let's discover new TypeScript concepts by looking at how Partial<T> and Omit<T, K> operates.