TypeScript

Understanding "as unknown as" in TypeScript

Picture of me, Omari
Omari Thompson-Edwards
Mon Jan 22 2024 5 min

Handling TypeScript types can be hard - Sometimes, despite your best efforts, you'll need some way to escape from TypeScript's rigid type system. One of these tools is the handy "as unknown as" cast - a little trick to convert between types TypeScript thinks you shouldn't be able to convert between. Despite seeing it in quite a few codebases, I haven't seen much about this handy trick online, so in this article we'll cover how to use it, and what scenarios it helps with.

What is Unknown in TypeScript?

The short version is that unknown represents a type that means "I don't know what this is, so I won't assume it can do anything". It's similar to another type you might be familiar with - the "any" type, which means "I don't know what this is, so I'll assume it can do anything". 

Casting.

The next part of this little trick uses type casting. Simply put, type casting lets you tell TypeScript to treat a variable as a different type, as long as there is some overlap. This is where "as unknown as" comes in handy - unknown has no known properties, so we can cast it to everything, and cast everything to it.

What does overlap mean here? Essentially, for casting type A to type B, either A has to be a subtype of B, or B has to be a subtype of A. So this works:

type TypeA {
    foo: string;
}

type TypeB {
    foo: string; 
    bar: string;
}

function useB(b: TypeB) {}

const myA: TypeA = {foo: 'foo'};

useB(myA as TypeB)

Because TypeB is just TypeA with an extra property

And it works if we swap the extra property to type A:


type TypeA {
    bar: string;
    foo: string;
}

type TypeB {
    foo: string; 
    
}

function useB(b: TypeB) {}

const myA: TypeA = {foo: 'foo'};

useB(myA as TypeB)

But if we add an extra property so that neither is now a subtype:

type TypeA {
    foo: string;
    baz: string;
}

type TypeB {
    foo: string; 
    bar: string;
}

function useB(b: TypeB) {}

const myA: TypeA = {foo: 'foo', baz: 'baz'};

useB(myA as TypeB) 
/*
Conversion of type 'TypeA' to type 'TypeB' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
  Property 'bar' is missing in type 'TypeA' but required in type 'TypeB'.
*/

as unknown as.

So you might have seen this in a codebase and wondered, "What's this for - why are we casting twice?". It's known as a double assertion, and it's a little TypeScript hack to convert between types TypeScript doesn't think we can convert between. "as unknown as" lets you cast a variable to unknown, and then to a second type, since unknown can be cast to anything, e.g.:

type BarType = {
    bar: string;
};

type FooType = {
    foo: string;
};

const bar: BarType;

const foo = bar as FooType; //❌ Conversion of type 'BarType' to type 'FooType' may be a mistake because neither type sufficiently overlaps with the other.
const foo = bar as unknown as FooType; //✅

It's unsafe because there's likely a reason why TypeScript is saying you can't cleanly convert between two types, but it can have its use cases. For example in our case, we can see there are no properties in common between them.

One case where it might be useful is if two types have some shared properties and those are the only ones you plan to access. For example, if we have these two types:

type User = {
    name: string;
    friends: User[];
    login: () => void;
}


type Admin = {
    name: string;
    login: () => void;
    updateUser: () => void;
}

Neither is a subtype of the other, they both have some unique properties.

So this won't work:

const myAdmin: Admin;

function greet(user: User) {
    console.log(`Hi ${user.name}`)
}

greet(myAdmin as User)

Even though we know both types have a ".name" property. But we can use "as unknown as":

greet(myAdmin as unknown as User)

And our error goes away. It's really important to note that we aren't creating any properties here - just casting the type. That's why you'll mainly see it used where there is some crossover. If you cast a variable to a type with no shared properties, TypeScript isn't going to create the new properties, you just won't see any errors before runtime when you try to access them. If you cast a variable to a type where there are some properties in common, at least you know those ones do exist.

as any as.

This is essentially the same thing, but you'll probably see "as unknown as" more often. The code is equivalent - cast to something that can be cast to anything, and then cast to something more specific.

type BarType = {
    bar: string;
};

type FooType = {
    foo: string;
};

const bar: BarType;

const foo = bar as FooType; //❌ Conversion of type 'BarType' to type 'FooType' may be a mistake because neither type sufficiently overlaps with the other.
const foo = bar as any as FooType; //✅

Like I said there's no difference, other than the fact that a lot of linters include rules forbidding any.

Conclusion.

Thanks for reading! You might not run into this pattern often - it's an escape hatch, and ideally, you won't need it in the first place. It probably shouldn't be your first choice, and is generally a sign of something going wrong. Hopefully, thanks to this article though, you're able to see how and why it's used. If you liked this article, feel free to share it, or why not check out some more of my articles.

read more.

Me
Hey, I'm Omari 👋

I'm a full-stack developer from the UK. I'm currently looking for graduate and freelance software engineering roles, so if you liked this article, feel free to reach out.