GianlucaBookshelfBlog

2023-07-27

Unsoundness with TypeScript Getters

https://assets.tina.io/02d04b15-35e4-489b-ad51-13f6dee14a94/unsoundness-with-typescript-getters/typescript fire dog.jpg

While reading The Seven Sources of Unsoundness in TypeScript I was thinking other methods to trick TypeScript in inferring the incorrect type. Turns out you can use getters to trick typeguards into thinking a variable has a certain type at compile time, while at runtime it has another.

We can exploit the TypeScript behavior that expects a getter to return the last value that was set on an object in the same synchronous scope and the fact that a getter is a function that can return anything it wants at runtime.

The following code will compile without problems, but at runtime it will call callAliveCat() with a DeadCat half of the times.

1class Box {
2    get observe(): AliveCat | DeadCat {
3        if (Math.random() > 0.5) {
4            return { name: 'Silvestro', meow: true };
5        } else {
6            return { name: 'Grumpy Cat', doNothing: true }
7        }
8    }
9}
10
11function callAliveCat(cat: AliveCat) {
12    console.log(cat.meow);
13}
14
15function f() {
16    const box = new Box();
17    if (isAlive(box.observe)) {
18        // Half the times the cat is dead, 
19        // but TypeScript thinks it's always alive
20        callAliveCat(box.observe);
21    }
22}

For the entire if block the compiler thinks box.observe is AliveCat, just because it was when isAlive() was invoked.

If we make observe() a function instead everything works correctly.

1function f() {
2    const box = new Box();
3    if (isAlive(box.observe())) {
4        // Now this is a compile error
5        callCat(box.observe());
6    }
7}

In my opinion TypeScript should be fixed to keep into account this edge case rather than assuming the developer never implements dynamic getters.