To master your TS skills you need to delve into the topic of types and explore their full possibilities. This article is dedicated only to types in particular.
So to improve your skills and to study how the functionality of types can improve the development, I strongly recommend at least getting acquainted with:
- Generics
- Template literal
- Indexed Access Types
- Utility types
- KeyOf/TypeOf operators
- Conditional types
- Mapped types
While learning current examples will be used for each case:
type Todo = {
text: string,
type: "very urgent" |"urgent" | "not urgent",
daysToFinish: number,
isFinished: boolean
}
- Pay attention that provided information is not full as not intended to be and it only describes the most popular use cases. You can find full info that covers 100% of the topic you can find on the official TS website.
And now let’s start with…
Generics
The main aim of generics is to provide re-usability.
Let’s learn with our todo example.
type Todo = {
text: string,
type: "very urgent" |"urgent" | "not urgent",
daysToFinish: number,
isFinished: boolean
}
Here no generics are used and each property has its own defined type.
And here generic is implemented:
type FirstTodo<T> = {
text: T,
type: "very urgent" |"urgent" | "not urgent",
daysToFinish: number,
isFinished: boolean
}
What does T mean?
T that provided is the triangle brackets defines type and text has the same T type.
For example, we want to give a type string for text property (that’s logical) and we have no problems with TS.
const firstTodo: FirstTodo<string> = {
text: "first todo",
type: "urgent",
daysToFinish: 6,
isFinished: false
}
And now let’s add generics for each of the property:
type SecondTodo<S, T, N, B> = {
text: S,
type: T,
daysToFinish: N,
isFinished: B
}
type TypeOptions = "very urgent" |"urgent" | "not urgent"
const secondTodo: SecondTodo<string, TypeOptions, number, boolean> = {
text: "first todo",
type: "urgent",
daysToFinish: 6,
isFinished: false
}
Here we also set logical types for each property, but for example, we set for B not boolean but a number that will mean that “isFinished” should equal to the number. As well for “T” we set his own custom type that could be changed with another type.
Template literal
It works rather similar to JS and here we create a special type to collect all levels of urgency.
type UrgencyRate = "very " | "" | "not "
type ThirdTodo<T> = {
text: T,
type: `${UrgencyRate}urgent`,
daysToFinish: number,
isFinished: boolean
}
const thirdTodo: ThirdTodo<string> = {
text: "third todo",
type: "not urgent",
daysToFinish: 6,
isFinished: false
}
Indexed Access Types
With Indexed Access we can create a type that can access a type of property of another type by putting one (or some) names of the properties in the square brackets.
type Todo = {
text: string,
type: "very urgent" |"urgent" | "not urgent",
daysToFinish: number,
isFinished: boolean
}
type TodoText = Todo["text"] // string
type TodoTextAndIsFinished = Todo["text" | "isFinished"] // string | boolean
With keyOf operator get all the properties types for 1 element.
type AllTypes = Todo[keyof Todo] // string | number | boolean
And with typeOf operator we can get the type.
typeof "todo" // console.log -> string
It is the way how to create a new type according to the current element.
const forthTodo: Todo = {
text: "forth todo",
type: "not urgent",
daysToFinish: 6,
isFinished: false
}
type AllTodoTypes = typeof forthTodo
// type AllTodoTypes = {
// text: string;
// type: "very urgent" | "urgent" | "not urgent";
// daysToFinish: number;
// isFinished: boolean;
}
Utility types
We will observe only 6 Utility types and they are Required, Readonly, Partial, Record, Pick, Omit and NonNullable.
Readonly doesn’t allow to change the object from outside.
type Todo = {
text: string,
type: "very urgent" |"urgent" | "not urgent",
daysToFinish: number,
isFinished: boolean
}
const fifthTodo: Readonly<Todo> = {
text: "fifth todo",
type: "not urgent",
daysToFinish: 6,
isFinished: false
}
fifthTodo.text = "new text" // Cannot assign to 'text' because it is a read-only property.
Required and Partial (Required vs Partial)
Required and Partial define if all properties of the type are mandatory.
type Todo = {
text: string,
type: "very urgent" |"urgent" | "not urgent",
daysToFinish: number,
isFinished: boolean
}
const todoWithRequired: Required<Todo> = {
text: "todo",
type: "very urgent",
daysToFinish: 10,
isFinished: true
}
const todoWithPartial: Partial<Todo> = {
text: "todo",
type: "very urgent"
}
Record (Record) is a constructor that helps to create a new type as an object of properties and types.
type TodosText = "first todo" | "second todo"
type TodoDescription = {
type: "very urgent" |"urgent" | "not urgent",
daysToFinish: number,
isFinished: boolean
}
const todos: Record<TodosText, TodoDescription> = {
"first todo": {
type: "not urgent",
daysToFinish: 10,
isFinished: false
},
"second todo": {
type: "urgent",
daysToFinish: 0,
isFinished: false
}
}
Pick and Omit (Pick vs Omit)
They help to create a new type by picking/or omitting the properties.
type Todo = {
text: string,
type: "very urgent" |"urgent" | "not urgent",
daysToFinish: number,
isFinished: boolean
}
type todoWithPick = Pick<Todo, "text" | "isFinished">
const todoPick: todoWithPick = {
text: "todo",
isFinished: true
}
type todoWithOmit = Omit<Todo, "isFinished">
const todoOmit: todoWithOmit = {
text: "todo",
type: "not urgent",
daysToFinish: 40
}
NonNullable creates new type by excluding undefined and null from the initial type:
type PossibleNullishTodo = {
text: string,
type: "very urgent" |"urgent" | "not urgent",
daysToFinish: number,
isFinished: boolean
} | null | undefined
type NotNullishTodo = NonNullable<PossibleNullishTodo>
// type NotNullishTodo = {
// text: string;
// type: "very urgent" | "urgent" | "not urgent";
// daysToFinish: number;
// isFinished: boolean;
}
Conditional types
Conditional types are aimed to define the relations between types.
type Todo = {
text: string,
type: "very urgent" |"urgent" | "not urgent",
daysToFinish: number,
isFinished: boolean
}
interface UrgentTodo extends Todo {
type: "very urgent"
}
type ConditionalType = UrgentTodo extends Todo? true : false // true
Mapped types
A mapped type is created on the base of the other types and helps to avoid repeating code. Let’s add readonly and to each property and make it optional while mapping.
type Todo = {
text: string,
type: "very urgent" |"urgent" | "not urgent",
daysToFinish: number,
isFinished: boolean
}
type customType<Type> = {
readonly [Property in keyof Type]?: Type[Property];
};
type ReadonlyPerson = customType<Todo>
//type ReadonlyPerson = {
//readonly text?: string | undefined;
//readonly type?: "very urgent" | "urgent" | "not urgent" | undefined;
//readonly daysToFinish?: number | undefined;
//readonly isFinished?: boolean | undefined;
}
That is all for types intro, I hope you enjoined it :)
Top comments (4)
Great work! Thanx!
Great article! I would cover type union as well 😊
Really helpful.
I like your article. Just want to add some clarity in the
Required
example.Todo
andRequired<Todo>
will behave quite similar considering your example? For instance, if you miss some property in thetodoWithRequired
object you will see the same type error.However if you set some properties as optional in
Todo
type (let it beisFinished?: boolean
or whatever), the different betweenTodo
andRequired<Todo>
will be more clear, I guess. The object with typeTodo
will be OK without specifying these optional properties, but notRequired<Todo>
.