DEV Community

Cover image for TypeScript Utility Types Complete Guide
Muhammad Lutfi
Muhammad Lutfi

Posted on

TypeScript Utility Types Complete Guide

Typescript Utility types

Utility types are helpers that typescript provides to make common type transformations easier.

For example if you have a Todo type

type Todo = {
  readonly id: number;
  title: string;
  description?: string;
  category?: string;
  status: "done" | "in-progress" | "todo";
  readonly createdAtTimestamp: number;
  updatedAtTimestamp: number | null;
};
Enter fullscreen mode Exit fullscreen mode

And you want to create a TodoPreview type that only has title, status properties.

Instead of creating a new type by hardcoding it, you can use

type TodoPreview = Pick<Todo, "title" | "status">;
// type TodoPreview = { title: string; status: TaskStatus }
Enter fullscreen mode Exit fullscreen mode

You might think "So why don't I just hardcode it?"

While hardcoding might work for small projects, it quickly becomes a Maintenance Nightmare in professional environments for two main reasons:

  • Intent & Readability: As in larger types you have to manually compare the two types to figure out how they are related while Utility types make the relationship clear
  • Scalability: As the codebase scales and requires changes constantly, keeping multiple hardcoded definitions in sync is a pain

this bring us to the core concept:

Single Source of Truth (SSOT)

For example if we have these types:

type Todo = {
  readonly id: number;
  title: string;
  description?: string;
  category?: string;
  status: "done" | "in-progress" | "todo";
  readonly createdAtTimestamp: number;
  updatedAtTimestamp: number | null;
};

type TodoPreview = {
  title: string;
  status: "done" | "in-progress" | "todo";
};

type TodoUpdate = {
  title?: string;
  description?: string;
  category?: string;
  status?: "done" | "in-progress" | "todo";
  updatedAtTimestamp?: number | null;
};
Enter fullscreen mode Exit fullscreen mode

And in a later version of the codebase we want to update status union type to "done" | "inProgress" | "todo", now you have to update each single definition for the status property inside each type. If you forget to update every single definition the types will become incompatible which will lead to bugs for sure

So we could prevent this type of problem by have Todo type as the only single source of truth and create the other types like this:

type Todo = {
  readonly id: number;
  title: string;
  description?: string;
  category?: string;
  status: "done" | "in-progress" | "todo";
  readonly createdAtTimestamp: number;
  updatedAtTimestamp: number | null;
};

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

type TodoUpdate = Partial<Omit<Todo, "id" | "createdAtTimestamp">>;
Enter fullscreen mode Exit fullscreen mode

Table of Content

Type Syntax Brief
Omit Omit<Type, Keys> excludes specific properties
Pick Pick<Type, Keys> picks only specific properties
Partial Partial<Type> makes all properties optional
Required Required<Type> makes all properties required
Readonly Readonly<Type> makes all properties readonly
Record Record<Keys, Type> defines object types dynamically
Exclude Exclude<UnionType, ExcludedMembers> excludes members from a union type
Extract Extract<Type, Union> extracts members from a union type
NonNullable NonNullable<Type> excludes nullish from a type
Awaited Awaited<Type> resolves a promise type
Parameters Parameters<Type> extracts a function type parameters
ReturnType ReturnType<Type> extracts a function return type
ConstructorParameters ConstructorParameters<Type> extracts a constructor function type parameters
InstanceType InstanceType<Type> constructs the instance type of a constructor function type
OmitThisParameter OmitThisParameter<Type> removes the this declaration from a function type
ThisParameterType ThisParameterType<Type> extracts the this declaration from a function type
ThisType ThisType<Type> overrides the this declaration of a function
Uppercase Uppercase<Type> converts all string type characters to uppercase
Lowercase Lowercase<Type> converts all string type characters to lowercase
Capitalize Capitalize<Type> converts first character of a string type characters to uppercase
Uncapitalize Uncapitalize<Type> converts first character of a string type characters to lowercase

Omit<Type, Keys>

takes an object of Type and excludes given keys from it Keys

type Todo = {
  readonly id: number;
  title: string;
  description?: string;
  category?: string;
  status: "done" | "in-progress" | "todo";
  readonly createdAtTimestamp: number;
  updatedAtTimestamp: number | null;
};
Enter fullscreen mode Exit fullscreen mode
type NewTodoInput = Omit<
  Todo,
  "id" | "createdAtTimestamp" | "updatedAtTimestamp"
>;
/*
type TodoUpdate = {
  title: string;
  description?: string;
  category?: string;
  status: "done" | "in-progress" | "todo";
}
*/

const createNewTodo = (todo: NewTodoInput) =>
  saveToDB({
    ...todo,
    id: Math.random(),
    createdAtTimestamp: Date.now(),
    updatedAtTimestamp: null,
  });

createNewTodo({
  title: "make article about utility types",
  status: "in-progress",
});
Enter fullscreen mode Exit fullscreen mode

Pick<Type, Keys>

creates a subset of object Type by including only given keys Keys

(the opposite of Omit)

type Todo = {
  readonly id: number;
  title: string;
  description?: string;
  category?: string;
  status: "done" | "in-progress" | "todo";
  readonly createdAtTimestamp: number;
  updatedAtTimestamp: number | null;
};
Enter fullscreen mode Exit fullscreen mode
type TodoPreview = Pick<Todo, "title" | "status">; // type TodoPreview = { title: string; status: TaskStatus }
Enter fullscreen mode Exit fullscreen mode

Partial<Type>

takes an object of Type and sets all its properties optional

type Todo = {
  readonly id: number;
  title: string;
  description?: string;
  category?: string;
  status: "done" | "in-progress" | "todo";
  readonly createdAtTimestamp: number;
  updatedAtTimestamp: number | null;
};
Enter fullscreen mode Exit fullscreen mode
type TodoUpdate = Partial<
  Omit<Todo, "id" | "createdAtTimestamp" | "updatedAtTimestamp">
>;
/*
type TodoUpdate = {
  title?: string;
  description?: string;
  category?: string;
  status?: "done" | "in-progress" | "todo";
}
*/

const updateTodo = (id: Todo["id"], update: TodoUpdate) => ({
  ...getTodo(id),
  ...update,
});

updateTodo(3, { status: "done" });
Enter fullscreen mode Exit fullscreen mode

Required<Type>

takes an object of Type and sets all its properties required

(the opposite of Partial)

type Options = {
  opt1?: boolean;
  opt2?: boolean;
  opt3?: number;
};

const userOptions = {};

const defaultOptions: Required<Options> = {
  opt1: true,
  opt2: true,
  opt3: 10,

  ...userOptions,
};
Enter fullscreen mode Exit fullscreen mode

Readonly<Type>

takes an object of Type and sets its properties readonly (meaning the properties cannot be reassigned)

type Options = {
  opt1?: boolean;
  opt2?: boolean;
  opt3?: number;
};

const userOptions: Readonly<Options> = {
  opt1: true,
  opt3: 10,
};

userOptions.opt2 = false;
// ~Error: Cannot assign to 'opt2' because it is a read-only property.
Enter fullscreen mode Exit fullscreen mode

Record<Keys, Type>

defines an object type where every key is a Keys type and every value is a Type type

type GeneralObject = Record<keyof any, unknown>;
Enter fullscreen mode Exit fullscreen mode

It can be used with union types to create an object type where every type in the union must have an associated value:

type UserRole = "admin" | "editor" | "guest";
type Permission = "create" | "delete" | "edit" | "browse";

type RolePermissions = Record<UserRole, Permission[]>;
/*
type RolePermissions = {
    admin: Permission[];
    editor: Permission[];
    guest: Permission[];
}
*/

const role1: RolePermissions = {
  admin: ["create", "delete", "edit"],
  editor: ["edit", "browse"],
  guest: ["browse"],
};
Enter fullscreen mode Exit fullscreen mode

in case you want to have the same utility but as optional union keys, you can combine utility types!

type PartialRolePermissions = Partial<RolePermissions>;
/*
type PartialRolePermissions = {
  admin?: Permission[] | undefined;
  editor?: Permission[] | undefined;
  guest?: Permission[] | undefined;
}
*/

const role2: PartialRolePermissions = {
  admin: ["create", "delete"],
};
Enter fullscreen mode Exit fullscreen mode

Exclude<UnionType, ExcludedMembers>

excludes members ExcludedMembers from a union type UnionType

type TodoStatus = "done" | "in-progress" | "todo";

type TodoUnCheckedStatus = Exclude<TodoStatus, "done">; // type TodoUnCheckedStatus = "in-progress" | "todo"
Enter fullscreen mode Exit fullscreen mode
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; x: number }
  | { kind: "triangle"; x: number; y: number };

type T = Exclude<Shape, { kind: "circle" }>;
// type T = { kind: "square"; x: number } | { kind: "triangle"; x: number; y: number };
Enter fullscreen mode Exit fullscreen mode

it excludes each member that includes or matches the given structure

so kind: "circle" works as unique identifier for the first union member just like y: number could be used as well to identify the third member kind: "triangle"

type T = Exclude<Shape, { y: number }>;
// type T = { kind: "circle"; radius: number } | { kind: "square"; x: number }
Enter fullscreen mode Exit fullscreen mode
type T = Exclude<Shape, { x: number }>;
// type T = { kind: "circle"; radius: number };
Enter fullscreen mode Exit fullscreen mode

Extract<Type, Union>

extracts (or picks) specific types that includes or matches the given structure Union from a larger union type Type

(the opposite of Exclude)

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; x: number }
  | { kind: "triangle"; x: number; y: number };

type T = Extract<Shape, { radius: number }>;
// type T = { kind: "circle"; radius: number };
Enter fullscreen mode Exit fullscreen mode

works like a filter that extracts whatever has a radius

NonNullable<Type>

excludes null and undefined from a type Type

type T0 = NonNullable<string | number | undefined>;
// type T0 = string | number

type T1 = NonNullable<string[] | null | undefined>;
// type T1 = string[]
Enter fullscreen mode Exit fullscreen mode

Awaited<Type>

takes a promise Type and represents whatever the promise resolves to

type Value = Awaited<Promise<string>>; // type Value = string

const value: Value = await Promise.resolve("hello");
Enter fullscreen mode Exit fullscreen mode

Parameters<Type>

Extracts the types used in the parameters of a function type Type in a Tuple.

declare function updateTask(id: number, data: TaskUpdate): Task;

type Params = Parameters<updateTask>;
// type Params = [number, TaskUpdate]
// type Params = [id: number, data: TaskUpdate]

type id = Params[0];
Enter fullscreen mode Exit fullscreen mode

ReturnType<Type>

creates a type of the return type of function type Type.

Note: For overloaded functions, this will be the return type of the last signature as typescript can't tell which signature is intended

declare function updateTask(id: number, data: TaskUpdate): Task;

type T0 = ReturnType<typeof updateTask>;
// type T0 = Task
Enter fullscreen mode Exit fullscreen mode

ConstructorParameters<Type>

Extracts the types used in the parameters of a class constructor function type Type in a Tuple.

(just like Parameters but for classes)

class Todo {
  readonly id: number;
  title: string;
  status: "done" | "in-progress" | "todo";

  constructor(id: number, title: string, status: Todo["status"]) {
    this.id = id;
    this.title = title;
    this.status = status;
  }
}
Enter fullscreen mode Exit fullscreen mode
type T0 = ConstructorParameters<typeof Todo>;
// type T0 = [number, string, "done" | "in-progress" | "todo"]
// type T0 = [id: number, title: string, status: "done" | "in-progress" | "todo"]

type T1 = ConstructorParameters<typeof String>;
// type T1 = [value?: any]
Enter fullscreen mode Exit fullscreen mode

InstanceType<Type>

returns the instance type of a constructor function (which is a class) Type.

class Todo {
  readonly id: number;
  title: string;
  status: "done" | "in-progress" | "todo";

  constructor(id: number, title: string, status: Todo["status"]) {
    this.id = id;
    this.title = title;
    this.status = status;
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: Classes are actually just a sugar syntax and compiles to constructor functions under the hood!

type T0 = typeof Todo; // Constructor Function Type
type T1 = InstanceType<typeof Todo>; // Instance Type
type T2 = Todo; // Instance Type as well
Enter fullscreen mode Exit fullscreen mode

Note: TS uses the class identifier itself as instance type type (just like we did in T2).

InstanceType<T> is used when type T is given dynamically:

function cookingMachine<T extends new (...args: any[]) => any>(
  Recipe: T,
): InstanceType<T> {
  return new Recipe();
}
// this is a function that takes Recipe argument of type T which is only a constructor function type
// returns an instance of given constructor `T`

const pizzaDish = cookingMachine(Pizza);
const burgerDish = cookingMachine(Burger);
Enter fullscreen mode Exit fullscreen mode

OmitThisParameter<Type>

returns the function type Type but with the this declaration in function parameters removed

Note: Read first about Declaring this in a function in Typescript

type DatabaseContext = {
  db: { save: (data: string) => void };
};

function saveUser(this: DatabaseContext, user: string) {
  this.db.save(user);
}

type SaveUserWithoutThis = OmitThisParameter<typeof saveUser>;
// type SaveUserWithoutThis = (user: string) => void
Enter fullscreen mode Exit fullscreen mode

ThisParameterType<Type>

extracts the type of this parameter declaration of a function Type

type DatabaseContext = {
  db: { save: (data: string) => void };
};

function saveUser(this: DatabaseContext, user: string) {
  this.db.save(user);
}

type T0 = ThisParameterType<typeof saveUser>;
// type T0 = DatabaseContext
type T1 = Parameters<typeof saveUser>;
// type T1 = [user: string]
Enter fullscreen mode Exit fullscreen mode

ThisType<Type>

injects or overrides the this type of a function

type ObjectDescriptor<D, M> = {
  data?: D;
  methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M
};

function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
  let data: object = desc.data || {};
  let methods: object = desc.methods || {};
  return { ...data, ...methods } as D & M;
}

let obj = makeObject({
  data: { x: 0, y: 0 },
  methods: {
    moveBy(dx: number, dy: number) {
      this.x += dx; // Strongly typed this
      this.y += dy; // Strongly typed this
    },
  },
});

obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);
Enter fullscreen mode Exit fullscreen mode

String Manipulation Types

These are types Typescript provide to help with string manipulation but within the type system!

Note: these types are intrinsic which means they are built-in to the compiler, unlike other types which are actually defined using Mapped Types and Generics

Uppercase<StringType>

type Upper = Uppercase<"helloWorld">; // type Upper = "HELLOWORLD"
Enter fullscreen mode Exit fullscreen mode

Lowercase<StringType>

type Lower = Lowercase<"heLloWoRld">; // type Lower = "helloworld"
Enter fullscreen mode Exit fullscreen mode

Capitalize<StringType>

type Cap = Capitalize<"helloworld">; // type Cap = "HelloWorld"
Enter fullscreen mode Exit fullscreen mode

Uncapitalize<StringType>

type Uncap = Uncapitalize<"HelloWorld">; // type Uncap = "helloWorld"
Enter fullscreen mode Exit fullscreen mode

References

Top comments (0)