TotalTypescript

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>
}