Asserts
interface User = {
id: string;
name: string;
}
interface MaleUser extends User {
gender: 'male';
}
function assertUserIsMale(user):asserts user is MaleUser {
if (user.gender!=='male') {
throw new Error('Failed asserting the user is a male')
}
}
Overload
Overload implements the polymorphic pattern in Typescript. Any method can be written with many signatures that ought to be compatible. Each overload definition hints the return type for a set of parameters
Type Predicate
Type predicate are used to keep track of a method output. In the example below, we have some logic in a method, then we reuse this method using a predicate so that Typescript has knowledge of the type of the data handled by the method
function assertUserIsValid(user):user is User {
return (typeof user === "object" &&
user !== null &&
"gender" in user &&
typeof user.gender === "string"
)
}
const user = getAUser()
if (assertUserIsValid(user)) {
return user.gender // we know that gender is not undefined!
}
Extends
We use extends in Typescript to describe the required definition for a type. As a result the type meets a constraints and this makes the code able to tell what data we are sure we can consume without having any undefined errors or invalid type error
Generics
Generic are a feature that extends the method signature to enable this signature to spread its parameters constraints within the method implementation. In practice,
function getMyMethod(args: any[]) {
return args.reduce((arr, item) => {
acc + item
})
}
function <T extends number>getMyMethod(args: T[]) {
function <T>getMyMethod(args: T[]) {
return args.reduce<T>((arr, item) => {
acc + item
})
}
Template literals to tranform an existing type
Typescript can transform a variable into a new variable to create new types dynamically.
The snippet below will build a types that list the getters in the User type. By using this template literal, we don’t duplicate the user properties. Also, should a new property be added to the User interface, the getters type does not require any change
interface User {
name: string;
age: number;
}
type UserGetters = {
[K in keyof User as `get${Capitalize<K>}`]: () => User[K]
}
Template literals to validate a list of routes in a Node environment
If we consider a method that has a route as parameter, we commonly would type the route parameter as string. With template literals, we can make the type of the route more accurate than string.
myRouter(myRoute: string);
becomes
myRouter(myRoute: `/${string}`); // now the route has to start with the character '/' (eg: '/home' is valid and 'anything' is not a valid route)
Type with Generic
It is common with Node API methods to receive different inputs and yet return a unified types. The benefit is to streamlined ways to consume this API. Yet, the inputs can be different and this proves to be versatile to enable many API to be built on the same pattern.
type CommentMethodResponse =
| { data: {commentId: number; author: string; content: string}}
| ErrorAPI
type ArticleMethodResponse =
| {data: {pageId: number; author: string; title: string; body: string}}
| ErrorAPI
The above is reasonable and is often seen used in our environments. Yet, if this definition was to be used in an environment that will have many more APIs, we could find a way to unify these 2 definitions further. The snippet below yields to a more readable definition and using less code on the long run
type EntityResponse<TData> = | data: TData | ErrorAPI
type CommentMethodResponse = EntityResponse<{commentId: number; author: string; content: string}>
type ArticleMethodResponse = EntityResponse<{pageId: number; author: string; title: string; body: string}>
Reusing or interface by using types transformations
Given a type definition, we can create subsets of it to ensure unique source of truth and enable further code to use the type whilst constraining it to specific requirements for safer uses of the type. For instance, if we want to publish all the user properties apart the id to ensure the user id cannot be propagated, we can use the following snippet
interface Building {
id: number;
storeys: number;
surface: number;
}
interface Bengalow = Pick<Building, 'surface'>
interface House = Omit<Building, 'id'>
interface BuildingArtifact = Partial<Omit<Building, 'id'>>
Use type union to intersect properties
type User = {
id: string;
name: string;
age: number;
}
type Company = {
id: string;
name: string;
numberStaff: number;
}
type User | Company will expose only id, name properties
Readonly keywords with typescript
Making object properties readonly, fullobject readonly or freeze the object or finally declaring a variable as const are all valid methods to define a variable immutable. However, one has a cons that is worth knowing.
function myMethod(users: readonly string[]) {
// users will not be modifiable within the scope of this method
}
const myObject = {
myProperty: 'test'
} as const
const myObject: Readonly<{myProperty: string}> = {
myProperty: 'test'
}
const newObject = myObject.freeze({
myProperty: 'test' // myProperty type will be string whilst the 2 previous snippets give myProperty type as the literal value 'test'
})
Adding import in files
This issue is more a knowledge about the IDE than typescript itself. Given that resolving import happens on every file of our codebase, having a way to quickly resolve a list of import is useful. Hover on the line that has no import, wait for the autocomplete hint to appear and resolve the missing imports in the file can make some wonders
Union Types
The union type is a versatile type definition that describe the possible values for a given data. The enum and union seem to be close to one another. But union has limitations as well as side-effects on the code and for performance too. Instead, union does not have these problems and is a good candidate for transformations so that it get reused, spread in the codebase without duplicating the type itself.
The example below is an illustration on how to reuse a union type to create the options for a select input in an HTML form
type Gender = "male" | "female" | "unisex" | "transgender"
type GenderOptions = {
[K in Gender]: Capitalize<K>
}