TypeScript assertion functions increase type safety

🌱February 16, 2024.
seedling 🌱
1 minute read ⏱

When working with union types, I often want to narrow the type to something specific. One way to do this is using assertion functions which assert that after they’ve been invoked, I can be certain that the type is what was asserted. The following assertion function will assert that a user is an admin:

function function assertIsAdmin(user: NormalUser | AdminUser): asserts user is AdminUserassertIsAdmin(user: NormalUser | AdminUseruser: type NormalUser = User & {
    role: "user";
}NormalUser | type AdminUser = User & {
    role: "admin";
}AdminUser): asserts user: NormalUser | AdminUseruser is type AdminUser = User & {
    role: "admin";
}AdminUser {
	if( "admin" !== user: NormalUser | AdminUseruser.role: "user" | "admin"role ) throw new var Error: ErrorConstructor
new (message?: string | undefined, options?: ErrorOptions | undefined) => Error (+1 overload)Error("user is not an Admin");
}
// Notice the arrow function syntax here
const const assertIsAdmin2: (user: NormalUser | AdminUser) => asserts user is AdminUserassertIsAdmin2 = (user: NormalUser | AdminUseruser: type NormalUser = User & {
    role: "user";
}NormalUser | type AdminUser = User & {
    role: "admin";
}AdminUser): asserts user: NormalUser | AdminUseruser is type AdminUser = User & {
    role: "admin";
}AdminUser => {
  if( "admin" !== user: NormalUser | AdminUseruser.role: "user" | "admin"role ) throw new var Error: ErrorConstructor
new (message?: string | undefined, options?: ErrorOptions | undefined) => Error (+1 overload)Error("user is not an Admin");
}

function function adminOnly(user: NormalUser | AdminUser): voidadminOnly(user: NormalUser | AdminUseruser: type NormalUser = User & {
    role: "user";
}NormalUser | type AdminUser = User & {
    role: "admin";
}AdminUser){
  assertIsAdmin2(user: NormalUser | AdminUseruser);
Assertions require every name in the call target to be declared with an explicit type annotation.
} function function deleteOrganization(user: AdminUser, organizationId: string): voiddeleteOrganization(user: AdminUseruser: type AdminUser = User & { role: "admin"; }AdminUser, organizationId: stringorganizationId: string){ // ... } function function attemptDeleteOrganization(user: NormalUser | AdminUser, organizationId: string): voidattemptDeleteOrganization(user: NormalUser | AdminUseruser: type NormalUser = User & { role: "user"; }NormalUser | type AdminUser = User & { role: "admin"; }AdminUser, organizationId: stringorganizationId: string){ function assertIsAdmin(user: NormalUser | AdminUser): asserts user is AdminUserassertIsAdmin(user: NormalUser | AdminUseruser); function deleteOrganization(user: AdminUser, organizationId: string): voiddeleteOrganization(
user: AdminUser
user
, organizationId: stringorganizationId);
}

assertIsAdmin asserts that the user is an AdminUser. There are a couple things to note here:

  1. The function keyword is required. Notice that using assertIsAdmin2, the version with an arrow function, causes an error. This is because of an implementation detail of the compiler; the control flow graph is computed before type inferencing.
  2. Any code that sits below a call to assertIsAdmin will have the type of user narrowed to AdminUser.

At the bottom in attemptDeleteOrganization, you can see that user’s type has been narrowed to AdminUser after the call to assertIsAdmin.