Typescript introduces a static type system at compile time. When transpiled to Javascript, the type information is lost, leaving only what JS inherently understands.
Using interface
or type
in Typescript does not have direct equivalents in Javascript. However, by utilizing classes with prototype chains, such as shown below, you can differentiate types at runtime using instanceof
.
class Car {
constructor(public readonly gears: number) {}
}
class Bike{
constructor(
public readonly gears: number,
public readonly model: string
) {}
}
let c: Bike | Car;
c = new Car(2)
console.log(c instanceof Car)
console.log(c instanceof Bike);
This approach addresses the runtime distinction between types but does not resolve the issue of accessing properties that may vary based on the specific type at compile time.
In cases where we have a union like Bike | Car
and attempt to access a property unique to one type while applied to another, a compile-time error would be generated.
console.log(c.model)
// ^^^^^ Property 'model' does not exist on type 'Car'
To address this challenge, when utilizing interface
, it's essential to provide mechanisms for discerning between various types. This allows Typescript to recognize when a variable's type is narrower than its declaration. If two types share identical shapes, distinguishing them becomes impossible without a discriminant property to provide subtle hints. In the provided example, Bike
differs from Car
due to its model
property, enabling us to create a 'type predicate' for both compile-time and runtime inference.
function isBike(obj: any): obj is Bike {
return typeof obj === 'object' && typeof obj.model === 'string'
}
We may also define an inverse function:
function isCar(obj: any): obj is Car {
return typeof obj === 'object' && typeof obj.model !== 'string'
}
Alternatively, we could consider:
function isCar(obj: any): obj is Car {
return typeof obj === 'object' && typeof obj.model === undefined
}
The method of discrimination between types is subjective and depends on the context. It is possible to base the differentiation on property values rather than just their types or presence within the object.
By adopting these type predicates, we safeguard the usage of Bike
's properties, promoting type safety during compilation and runtime execution.
if (isBike(c)) {
console.log(c.model)
}
if (isCar(c)) {
console.log(c.model)
// ^^^^^ Property 'model' does not exist on type 'Car'
}
Discriminated Unions present a similar solution. While type predicates rely on accurate implementation, there is a risk of inconsistencies when codebase modifications are made without updating the predicates accordingly, potentially resulting in misinterpretations. Nonetheless, this method offers flexibility in handling type unions lacking a common discriminator property.