Typescript “casts” are not casts

Mark Jordan
Ingeniously Simple
Published in
4 min readSep 16, 2021

At Redgate we recently ran an internal typescript training course, and I wanted to highlight a common misconception I saw a couple of times.

To explain what I mean, let’s compare some similar-looking code in C# and typescript. In C#, we can use the as operator (sometimes called a “safe” cast) to check the type of an object:

public void HandleEvent(object sender, Event event) {
var ourEvent = event as OurCustomEventType;
if (ourEvent != null) {
// ... handle event
}
}

Alternatively, we can also use a cast expression (or “explicit” cast) to throw an InvalidCastException if the cast fails:

var publishedDate = (DateTime)metadata["publishedDate"];
Console.WriteLine("Document published on " +
publishedDate.ToString("MMMM dd, yyyy"));

The important part is that these casts happen at runtime: the C# compiler emits instructions to:

Typescript, however, does none of these things. If you’re coming to typescript from another language, you may be surprised to find out that your intuition about casts doesn’t apply here.

While typescript has syntax that looks like casts, these are actually “type assertions” instead. Let’s take the first example, and write something like it in typescript:

const handleEvent = (sender: object, event: Event) => {
const mouseMove = event as MouseEvent;
console.log(`Mouse moved to ${mouseMove.x},${mouseMove.y}`);
}

This looks very similar to the first C# example — indeed, the syntax is almost identical. But if we take a look at the typescript playground, we can see that our code actually compiles to the following javascript:

    const mouseMove = event;

and the type assertion has disappeared completely. Type assertions always succeed. They’re a way for you to tell the typescript compiler “Suppress errors here, I know what I’m doing” which can be useful, but is more dangerous than you might expect. If you’ve cast to a type with more properties than the object actually has, then any later code will assume those properties exist — and you might see errors caused by impossible-looking undefined values.

Are type assertions completely unsafe? There is some minimal type checking for “unrelated” types. If the source type has nothing in common with the target type, then typescript will complain:

interface Cat {
numLives: 9;
}
interface Dog {
bark: () => void;
}
const animal = { bark: () => {} } as Cat
// ^ fails with an error: Conversion of type '{ bark: () => void; }' to type 'Cat' may be a mistake because neither type sufficiently overlaps with the other.

However, if we tried to compile { } as Cat , then typescript wouldn’t complain — because { } is a subset of the Cat interface.

So when should you use type assertions? I’d say the answer is as little as possible. Here are a couple of situations where they can be necessary:

  • When you genuinely know better than the compiler
    For example, Object.keys and Object.entries return stringly-typed keys instead of keyof typeof obj, because any object with more properties can satisfy that interface. But if that’s unlikely to happen in your specific code, then you can cast it back to the more specific string union type to get better type safety later on:
for (const [key, value] of Object.entries(dataRow)) {
renderCell(getHeaderIcon(key as keyof ModelType), value);
}
  • When handling external data
    If you’re receiving an external object from a web request or JSON.parse(), you probably already know what shape you expect the data to be in. If possible, you should implement type guards (or use a library like io-ts which automates a lot of runtime checking). But this may be overkill for many projects, and it’s a lot easier to just stick in a type assertion — especially since it’s an improvement over propagating the any type!
  • When mocking large objects in a test
    In test code, we can sometimes afford to be a little sloppier with types, because we’re not risking production code breaking, and we really care whether the test passes or not. For example, a mocked fetch call returns a Response object with a couple dozen properties but we might only care about calling .json(). In that case we need the type assertion just to satisfy typescript, even though we know it’s incorrect:
const result = { success: false, error: "Something exploded!" };
window.fetch = jest.fn().mockReturnValueOnce(
Promise.resolve({
json: async () => result
} as Response)
);

So, to sum up: typescript type assertions may be more dangerous than you may think, and you should avoid them if possible. But sometimes they turn out to be necessary! The important thing, as always, is to be properly informed about how they work and to make deliberate decisions when writing code.

Original photo by Tom Claes on Unsplash

--

--