Everyday TypeScript: As Is Dangerous
Welcome to the As Is Dangerous lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
In an earlier lesson, we used
asto override the type of a variable. This is helpful when working withanyorunknownvalues:asallows us to give these a "proper" type. Usingascan also help us port legacy JavaScript code to TypeScript quickly.Previously, we used
asto build an object one property at a time. We did this by telling TypeScript to treat our initial empty object as a different, larger object type.>
const user = {} as {name: string, likesOlives: boolean};user.name = 'Betty';user.likesOlives = false;user;Result:
It's possible to rewrite this example without using
as:>
const user = {name: 'Betty', likesOlives: false};user;Result:
{name: 'Betty', likesOlives: false}The
asversion works, so why consider replacing it? Becauseasis very dangerous: it overrides the type system! When we sayx as SomeType, we're telling the compiler: "xhas the typeSomeType, no matter what the rest of the code says, even if that type is obviously wrong." This is very different from normal type checking, where all of the types have to agree. Withas, we're allowed to assign types that are wrong!Let's build up an example. Normally, TypeScript catches mistakes where we try to access properties that don't exist. For example, we can't access the
.lengthof a number because numbers don't have lengths.>
const aNumber = 5;aNumber.length;Result:
If we use
as, we lose that safety. For example, we can tell TypeScript that a number is really a string. Withas, it will believe us. But it's still a number at runtime: the types are now wrong!>
const aNumber: unknown = 5;const aString = aNumber as string;aString;Result:
5
That example shows us that the runtime type of a value (the number 5) doesn't always match the TypeScript type (
string). There are many ways for this to happen, most of which are subtle. Butasis a case where it's very easy to introduce this mistake!Each use of
asis dangerous because it can lead to this kind of confusion around types. Worse, it can cause bugs far away from theasitself. For example, we might passaStringto other code, which will believe that it's a string. But when we try to use it as a string at runtime, we'll get the wrong results.Here's an example showing that. The next code example ends with the variable
length, which has the typenumber. But at runtime, thelengthvariable containsundefined, which violates its statednumbertype!>
const aNumber: unknown = 5;const aString = aNumber as string;const length: number = aString.length;length;Result:
undefined
Note that there's no
asin theconst length: number = aString.length;line of code. Once we've usedas, everything else proceeds as normal, andaStringis typed as a string even though that's wrong in this case.In short, this isn't a bug in TypeScript. The compiler is working as intended. It believed what we told it with
aNumber as string: "treat this as a string, no matter what the rest of the code says, even if that type is obviously wrong."String literal types are especially prone to mistakes with
as. If we have a string like'diamonds'in our code, TypeScript will infer its type asstring. That makes sense: it's a string!However, that inference causes problems when we're using literal types. Here's a
Cardtype that we'll use as an example.>
type Card = {rank: number | 'J' | 'Q' | 'K' | 'A'suit: 'clubs' | 'diamonds' | 'hearts' | 'spades'};Now here's some code that uses
Cardto list the trump suits in different card games. It looks like it should work, but it produces a type error.(In card games, a "trump suit" is a suit that's special in some way, usually by being more powerful. Bridge, Spades, and Sheepshead are games with trump suits.)
(You can write
type errorif an example will result in a type error.)- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const card: Card = { rank: 'Q', suit: 'clubs' };const games = {spades: 'spades',sheepshead: 'diamonds',};card.suit = games.sheepshead;card.suit;Result:
type error: Type 'string' is not assignable to type '"clubs" | "diamonds" | "hearts" | "spades"'.
The problem is that TypeScript inferred the type of our
gamesobject as{spades: string, sheepshead: string}. That inference makes perfect sense on its own. But when we try to assigngames.sheepsheadtocard.suit, we get a type error: we're trying to assign astringto a property that's a union of four specific literal string types.One common solution is to use
asto forcefully tell the compiler what type we want:'diamonds' as 'diamonds'.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const card: Card = { rank: 'Q', suit: 'clubs' };const games = {spades: 'spades' as 'spades',sheepshead: 'diamonds' as 'diamonds',};card.suit = games.sheepshead;card.suit;Result:
'diamonds'
That works, and it looks innocent on its own, but it's a bit dangerous. The problem is that TypeScript will trust our
as, even when it's wrong. For example, we might make a typo:'diamonsd'instead of'diamonds'. Here's some incorrect code that does exactly that.>
// Using the `as` operator allows us to assign a variable with the wrong type!const suit: 'diamonds' = 'diamonsd' as 'diamonds';suit;Result:
'diamonsd'
Now our card suit is
'diamonsd', which violates the allowed types we've defined. TypeScript only allows this because we usedas!Now for some good news: we can rewrite the
'diamonds'examples above safely, without usingas. In the next example, we add explicitSuitandTrumpSuitstypes. This lets the compiler know that thesheepsheadproperty holds aSuit, not a plain string.Here are the new types:
>
type Suit = 'clubs' | 'diamonds' | 'hearts' | 'spades';type Card = {rank: number | 'J' | 'Q' | 'K' | 'A'suit: Suit};type TrumpSuits = {spades: Suitsheepshead: Suit};Here's the code that uses our new type assignments, rather than
as:- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const card: Card = {rank: 'Q', suit: 'clubs'};const trumpSuits: TrumpSuits = {spades: 'spades',sheepshead: 'diamonds',};card.suit = trumpSuits.sheepshead;card.suit;Result:
'diamonds'
This is an example of a very important general principle in TypeScript. Sometimes, it seems like you have to defeat the type system to get something done. You might do that with
as, or withany, or with other less common TypeScript features. Usually, there's a safer way to do it, as we just saw! In many cases, adding more explicit types will resolve the error, while also making your code safer and more explicit.Fortunately, the TypeScript team have made
asmore safe over time. The compiler detects cases that seem obviously wrong, producing a type error rather than letting the code compile. For example, it often catches typos in object property names.>
type User = {name: string};const user: User = {naem: 'Gabriel'} as User;Result:
type error: Conversion of type '{ naem: string; }' to type 'User' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. Property 'name' is missing in type '{ naem: string; }' but required in type 'User'.(Note the unusual error message there. TypeScript said that our code may be a mistake, whereas it normally says "this is definitely a type error".)
TypeScript can't always warn us, as we saw above with the
'diamonsd'typo. Don't assume thatasis safe just because the compiler catches mistakes sometimes. The purpose of a static type system is to catch certain mistakes always, not sometimes!A final note about another common use of
as. It's tempting to useaswhen reading data from external systems like networks or files. When we receive some JSON data from an API server and expect it to be aCard, it's tempting to writeJSON.parse(incomingData) as Card.However, that's very dangerous and will lead to difficult bugs down the road. Even if our
Cardtype is correct today, the API server may change the structure of the objects over time. Or the API may have an outage in the future, returning an error object that doesn't match our expected type.When either of those things happens, we'll get runtime errors if we're lucky. If we're unlucky, the incorrect data may make it all the way into a database.
When consuming data from APIs, we recommend using a library like Zod. It lets us define static TypeScript types, but also check data against them at runtime. If the data doesn't match, we can handle that error case immediately rather than having incorrect data flow through the system.