2
votes

Why did TypeScript not implement Type Casting but only Type Assertion? I'm not looking for an answer for my code, but for the reason that Type Casting is not implemented in TypeScript, and why we not should (assumption!) implement it at our own.

Example. I've an TypeScript front-end, receiving JSON-data from the backend via AJAX-calls, having elements in elements in elements. It's about food, and you have to pay the price multiplied by the hour of the day.

We got this JSON:

{
    "food" : [{
            "name" : "pizza",
            "price" : 1.234
            "ingredients" : [
                "name" : "cheese",
                "extra_price" : 1.2345
            ]
        }
    ]
}

And we have these classes:

class Food {

    public name : string;
    public price : number;
    public ingredients : Ingredient[];

    public timePrice() : number {
        return this.price * (new Date()).getHours();
    }
}

class Ingredient {
    public name : string;
    public extra_price : number;
}

If we Cast this types using TypeScript, we can perfectly use the properties, including the ingredients. Perfectly.

But: We can't use the function timePrice. Because TypeScript does Type Assertion and NOT Type Casting.

I'm aware of the option that you could write a constructor creating the class when you pass the properties as parameters, but if you have elements in elements in elements: not an option. So the only option to fix this for me is to create a Utils class with static function and Food and Ingredient as parameters. That's working.

But why? What is the problem with Type Casting that it isn't implemented in TypeScript? It seems not so hard to me to build it, but because MicroSoft did not do this, there must be insurmountable problems by doing this.

1
Type casting is not what you think it is. With type casting, all you would have is a runtime exception saying that the JSON object is not an instance of Food. What you want is JSON unmarshalling/binding to classes. That would be the role of a library, not a language (especially not a language that is supposed to transile to JavaScript and run without any specific runtime library). - JB Nizet
@JBNizet 100% spot on. I would add that even in languages that do support this directly, it's generally not a common practice to add significant behaviour to data transfer/models constructs. So while the OP could use a library I would not recommend it. It's best to keep functionality out of these structures - Aluan Haddad
also keep in mind that typescript does not add any runtime checks. it does it's magic at compile time only. That's also one of the reasons there is no type casting in Typescript. - toskv

1 Answers

2
votes

For a top-level object with primitive properties, like the one you have here, you could certainly just mutate the prototype of the object to point to the class prototype and most things would work like you expect.

But what about a class like this?

class Food extends Something {
   name: string;
   price: number;
   eat = () => { super.consume(); };
}

Here, each time we call new Food(), we'll get a new closure for the eat property. Now a compile-time cast to Food needs to create a new closure, somehow find a proper reference to the consume method of the superclass (this is not trivial - it may not be reachable from the current scope!), rebind it (this can't be done with 100% fidelity in ES6), and copy it over. So there's one problem off the bat.

It gets worse. Let's say you have a group of classes like this:

class ShoppingCart {
  items: Array<Food | Sundry>;
}
class Food {
  name: string;
  calories?: number;
  eat() { }
}
class Sundry {
  name: string;
  brand?: string;
  use() { }
}

Then you write

var x: ShoppingCart = <ShoppingCart>{ items: [
  { name: 'ace' },
  { name: 'avocado', calories: 130 },
  { name: 'triscuits', brand: 'kraft', calories: 100 }
]};

This is getting worse! Now we have to mess with the root object, and iterate the object to set their prototypes. That's getting really complicated. But it's still worse... the { name: 'ace' } element is both a valid Food and a valid Sundry! Which do we cast it to? And the triscuits element is also ambiguous - what are we supposed to do?

And this still gets worse -- we may not have any way to get a runtime reference to Food or Sundry from this code. You may have only imported ShoppingCart from some module and not imported Food or Sundry.

We still haven't covered problems like

  • How do we run initializers from ambient classes?
  • What about private and protected members?
  • What about classes that run important logic in their constructors?
  • How do getters and setters interact with this?