TypeScript assertion functions increase type safety

🌱 February 16, 2024.
seedling 🌱
2 minutes 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, options?: ErrorOptions) => 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, options?: ErrorOptions) => 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.