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;
};
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 }
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;
};
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">>;
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;
};
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",
});
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;
};
type TodoPreview = Pick<Todo, "title" | "status">; // type TodoPreview = { title: string; status: TaskStatus }
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;
};
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" });
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,
};
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.
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>;
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"],
};
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"],
};
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"
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 };
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 }
type T = Exclude<Shape, { x: number }>;
// type T = { kind: "circle"; radius: number };
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 };
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[]
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");
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];
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
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;
}
}
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]
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;
}
}
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
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);
OmitThisParameter<Type>
returns the function type Type but with the this declaration in function parameters removed
Note: Read first about Declaring
thisin 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
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]
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);
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"
Lowercase<StringType>
type Lower = Lowercase<"heLloWoRld">; // type Lower = "helloworld"
Capitalize<StringType>
type Cap = Capitalize<"helloworld">; // type Cap = "HelloWorld"
Uncapitalize<StringType>
type Uncap = Uncapitalize<"HelloWorld">; // type Uncap = "helloWorld"
Top comments (0)