Advanced typescript 👑

Each one of us wants to be better from day to day and today’s story is about making us better. We will talk about typescript and more advanced topics that should help us write better code in the future.

As const assertions

Assertions are a way to make variables more specific regardless of their inferred type. One of them is as const assertions, which make our variable readonly.

const numbers = [1,2,3] as const;
//Cannot assign to '0' because it is a read-only property.ts(2540)
numbers[0] = 1;

Above we want to change the value of the first element of the array. The compilator will raise the error. The same story will be with objects.

const test = {test: 'test'} as const;
//Cannot assign to 'test' because it is a read-only property.ts(2540)
test.test = 'test2';

We have to be aware that this assertion works only in compilation time, at runtime we can still change the value, to be sure that the object is immutable we have to call also Object.freeze method.

Satisfies Keyword

Sometimes we want to ensure that the object contains only desirable elements, but on the other end, we want to let the typescript infer a specific value.

type Colors = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number]
const colors: Record<Colors, string | RGB> = {
    red: "red",
    green: "green",
    blue: [0,0,255]
}
//Property 'toUpperCase' does not exist on type 'string | RGB'. Property 'toUpperCase' does not exist on type 'RGB'.ts(2339)
const testString = colors.red.toUpperCase();
const testArray = colors.blue;

The above example will raise an error because typescript doesn’t know if the property is string or array. To fix this we can do the casting.

const testString = (colors.red as string).toUpperCase();

but it doesn’t look good. What we can do instead is to use satisfies Keyword.

type Colors = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number]
const colors = {
    red: "red",
    green: "green",
    blue: [0,0,255]
} satisfies Record<Colors, string | RGB>

const testString = colors.red.toUpperCase();
const testArray = colors.blue;

in that way, we check only if the object is built using available types.

Generic Types

Generic types allow you to write functions and classes that can work with multiple data types.

function getAge<T>(age: T): T {
  return age;
}

const testString = getAge("25").toUpperCase();
const testNumber = getAge(25);
class Pair<T,V> {
    private first: T;
    private second: V;
    constructor(first: T, second: V){
        this.first = first;
        this.second = second;
    }
    getValues(){
        return `${this.first} ${this.second}`
    }
}

const pair = new Pair<string, string>("first", "second");
const pair2 = new Pair<number, number>(1, 2);

We can constraint generic to a specific group of objects using extends Keyword.

interface Point {
    x: number,
    y: number
}

function getCoordinates<T extends Point>(value: T): string {
    return `${value.x} ${value.y}`;
  }

const test1 = getCoordinates({width: 200, x: 3, y: 3});
//Object literal may only specify known properties, and 'width' does not exist in type 'Point'.ts(2345)
const tes2 = getCoordinates({width: 100, x: 30});

And more complex example with the keyof operator that returns keys of the given object.

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
    return obj[key];
  }

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a");
//Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.ts(2345)
getProperty(x, "m");

Utility types

Some more useful Utility types in typescript.

  • Partial

Makes properties optional.

interface User {
  name: string;
  age: number;
  email: string;
}

function createUser(user: Partial<User>): User {
  return {
    name: "John Doe",
    age: 30,
    email: "john.doe@example.com",
    ...user,
  };
}

const newUser = createUser({ name: "Jane Doe" });
  • Readonly

Makes properties readonly. We can accomplish the same behavior with as const Keyword.

interface Todo {
  title: string;
}

const todo: Readonly<Todo> = {
  title: "Delete inactive users",
};

// Cannot assign to 'title' because it is a read-only property.
todo.title = "Hello";
  • Pick

Pick specific properties from the type and create a new type.

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
};
  • Omit

Omit specific properties and create a new type.

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type Description = Omit<Todo, "title" | "completed">;

const todo: Description = {
  description: "Clean room desc",
};

Mapped Types

Mapping functionality in programming means transforming some object into another in such a way that the resulting object is the same size as the original, like map function in js.

[1, 2, 3].map((item) => item + 1);

In typescript, Mapped types allow us to transform one type into another. It is useful when we have some relation between objects that need to be synced.

interface User {
  name: string;
  password: string;
}

type UserPermission = {
  editName: boolean,
  editPassword: boolean,
};

Above we have User type and the corresponding UserPermission. Every time we add sth. to the User type we need to add corresponding permission to it. We can make the typescript do it for us instead.

type UserPermission = {
  [key in keyof User as `edit${Capitalize<key>}`]: boolean
}

const permission: UserPermission = {
  editName: true,
  editPassword: true
}

In that way, every time then we add sth. to the User, the UserPermission type will be extended with the corresponding permission.

Conditional Types

Conditional types allow us to define type using condition.

type SomeType<T> = T extends string ? string : any;
type StringType = SomeType<"test">;
type AnyType = SomeType<1>;

We can combine this expression with infer keyword. In that way, we can get literal from the value.

type InferSth<T> = T extends {a: infer A} ? A : any;
//type Inferred = "test"
type Inferred = InferSth<{a: "test"}>;

A couple of utility types use under-the-hood conditional types. Let’s check how they are implemented.

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type ReturnTypeExample = ReturnType<() => 1>; // type ReturnTypeExample = number

type Exclude<T, U> = T extends U ? never : T;
type T2 = Exclude<string | number | (() => void), Function>;  // string | number

type Extract<T, U> = T extends U ? T : never;
type T0 = Extract<"a" | "b" | "c", "a" | "f">; // type T0 = "a"