Execute Program

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 as to override the type of a variable. This is helpful when working with any or unknown values: as allows us to give these a "proper" type. Using as can also help us port legacy JavaScript code to TypeScript quickly.

  • Previously, we used as to 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}Pass Icon
  • The as version works, so why consider replacing it? Because as is very dangerous: it overrides the type system! When we say x as SomeType, we're telling the compiler: "x has the type SomeType, 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. With as, 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 .length of 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. With as, 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:
    5Pass Icon
  • 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. But as is a case where it's very easy to introduce this mistake!

  • Each use of as is dangerous because it can lead to this kind of confusion around types. Worse, it can cause bugs far away from the as itself. For example, we might pass aString to 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 type number. But at runtime, the length variable contains undefined, which violates its stated number type!

  • >
    const aNumber: unknown = 5;
    const aString = aNumber as string;
    const length: number = aString.length;
    length;
    Result:
    undefinedPass Icon
  • Note that there's no as in the const length: number = aString.length; line of code. Once we've used as, everything else proceeds as normal, and aString is 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 as string. That makes sense: it's a string!

  • However, that inference causes problems when we're using literal types. Here's a Card type 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 Card to 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 error if 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"'.Pass Icon
  • The problem is that TypeScript inferred the type of our games object as {spades: string, sheepshead: string}. That inference makes perfect sense on its own. But when we try to assign games.sheepshead to card.suit, we get a type error: we're trying to assign a string to a property that's a union of four specific literal string types.

  • One common solution is to use as to 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'Pass Icon
  • 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'Pass Icon
  • Now our card suit is 'diamonsd', which violates the allowed types we've defined. TypeScript only allows this because we used as!

  • Now for some good news: we can rewrite the 'diamonds' examples above safely, without using as. In the next example, we add explicit Suit and TrumpSuits types. This lets the compiler know that the sheepshead property holds a Suit, 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: Suit
    sheepshead: 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'Pass Icon
  • 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 with any, 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 as more 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'.Pass Icon
  • (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 that as is 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 use as when reading data from external systems like networks or files. When we receive some JSON data from an API server and expect it to be a Card, it's tempting to write JSON.parse(incomingData) as Card.

  • However, that's very dangerous and will lead to difficult bugs down the road. Even if our Card type 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.