TypeScript: type compatibility vs duck typing
Tags: typescript, webdev
Published: 2023-04-17 10:54:01

## Intro
TypeScript is based on the **structural** type system. In a nutshell, the types of variables involved in some operations in your code should not be explicitly "identical". TypeScript considers the operation valid if types have a similar structure and follow the same shape.

I'm not going to provide a comprehensive article on type compatibility. The goal is to share with you a small research on a side-effect caused by this feature. Namely an issue with duck typing.

## Compatible types example
First, let's look at the following piece of code

```typescript
interface Pizza {
  name: string,
}

interface Beer {
  name: string;
  isDark: boolean;
}

let pizza: Pizza = { name: 'Gangsta Paradise'};
let beer: Beer = { name: 'Pirate Rage', isDark: true};

pizza = beer; // valid operation because Pizza and Beer are compatible
```
In the last line, we assign the `Beer` data to a variable having the `Pizza` type. `Beer` and `Pizza` are separate, completely independent interfaces. But TypeScript considers them as compatible and allows us to put new data in the `pizza` variable.

In a real app such an assignment might be an issue causing mistake or vice versa a smart trick based on good understanding of TypeScript features. For our research, the idea of this assignment is not important. We only care that it is valid for Typescript.


## Duck Typing
Let's proceed with writing code for our app. Both `Pizza` and `Beer` get a new numerical property `price`. Also, in specific cases, we want to make some discount on products. Let's say 10% discount on pizzas and 20% on beers. 

Following SOLID principles, we want to put the discount calculation logic in a separate function. The function is supposed to deal with all types of products. Something like this
```typescript
interface Pizza {
  name: string,
  price: number,
}

interface Beer {
  name: string,
  isDark: boolean,
  price: number,
}

function getDiscountPrice(product: Pizza | Beer): number{
  const isBeer: boolean = //??? 
  if(isBeer){
    // beer gets 20% off
    return product.price * 0.8
  } else{
    // pizza gets 10% off
    return product.price * 0.9
  }
}
```
Pay attention to the `isBeer` flag. We want to run different logic for pizzas and beers. But how to find out, which type of product the function is dealing with? 

If `Pizza` and `Beer` would be classes we could apply the `instanceof` operator. But unfortunately, they are interfaces and  `instanceof` would not work.

**Duck typing** to the rescue! According to the interfaces we defined, `Beer` has the `isDark` property, but `Pizza` don't. So we can calculate `isBeer` like
```typescript
function getDiscountPrice(product: Pizza | Beer): number{
  const isBeer = 'isDark' in product;
  if(isBeer){
    // beer gets 20% off
    return product.price * 0.8
  } else{
    // pizza gets 10% off
    return product.price * 0.9
  }
}
```

> Note
> It would be better to wrap `isBeer` into a type guard. But let's keep it as it is for simplicity

## Issue with duck typing for compatible types
Let's put together the `getDiscountPrice` function and the type compatibility case we studied in the beginning.

```typescript
interface Pizza {
  name: string,
  price: number,
}

interface Beer {
  name: string,
  isDark: boolean,
  price: number,
}

let pizza: Pizza = { name: 'Gangsta Paradise', price: 100 };
let beer: Beer = { name: 'Pirate Rage', isDark: true, price: 100 };

pizza = beer;                          // (1)

const price = getDiscountPrice(pizza); // (2)
console.log(price);                    // price is 90

function getDiscountPrice(product: Pizza | Beer): number{
  const isBeer = 'isDark' in product;
  if(isBeer){
    // beer gets 10% off
    return product.price * 0.9
  } else{
    // pizza gets 20% off
    return product.price * 0.8
  }
}
```
After the assignment in line `(1)` the variable `pizza` is still typed as `Pizza`. Given the code of `getDiscountPrice` we might expect that the calculated discount price will be `80` (since any `Pizza` is supposed to get 20% off). 

But actually, the calculated price will be `90`. We have `90` because the pizza data happened to have the `isDark` property which is unexpected for our duck typing approach. You can run this example and check the result in [TypeScript Sandbox](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgArAF4bsg3gKGWRDgFsIAuZAZzClAHMAaQ5AB3qSpAFdSAjaCwC++fKEixEKAEIRoeViXJVa9EM1bBqAEThQA1lX4B7EwBsIcECyIdgXYn0FQRYy2HaZsVdFhwAvHjEZJTIAOQA4tYMtDio+nAAJtoQ4UzsnGEAjAAMucjCANz4HsiC0FRyCkG4ISoR6FBwkMgASnAMaRnaeoZUdDwQGfaOeQXFYmzegeXyUEVES8srq6sA9OvIABTZAJRiCCYgtJkOKEFdYDraRzzgqFnb0-57i5s7AEwHRycWEAA6cwmBjPLJvNaQlYfbYAZgO+Bg9wQYGAx2QVxu1DuDyeHBMSR4KN8M2QAB9kNUoHtuM5oAQiL9TtoqcgguFevoDOFkKBMgSiWASkRgDBtiz5nsGUsPhUoBiIGBqMg8gBSZAmGAwVhEKCKnhQED8wkogGjFAAKmQuQBAE5WMJkBBzNQINKiB8XtgFUrkJ9curNdrlnqwAajfiTWAzVlkFabQAOB34YRAA)

You can rightly say "_Wait a minute, the code works as expected. You, as a developer, put some strange data in the pizza variable in the line `(1)`. Don't blame TypeScript_"

Well, I know, I agree with you. That's the developer's fault. But the TypeScript let me do it, no compilation error was thrown. Type compatibility system considers code like `(1)` as valid.

It might be a problem in a real-world big app. Imagine that pieces of code `(1)`, `(2)`, and the function declaration for `getDiscountPrice` sit in different modules. The data flow and modules interaction is complicated. But the TS compilation reveals no errors. Valid type compatibility might result in a tricky issue if the assignment on `(1)` is made by mistake. 

## Conclusion
TypeScript is a great tool. Its type checking is able to detect mistakes in code making the codebase safer. But TypeScript is not able to reveal all the flaws. For me, one of the most severe drawbacks of Typescript is the fact that the border between "detectable" and "non-detectable" mistakes is sometimes blurred.

I mean the code above is absolutely valid from the TS perspective. And still, it has some unexpected, error-prone behavior.

Know the tools you use. I obtained deeper understanding of TypeScript when studying this case. I hope it will be helpful for you as well.