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.

In attemptDeleteOrganization, you can see that after the call to assertIsAdmin, the user’s type is narrowed to AdminUser.