TypeScript Duck Typing, Want Strong Static Typing
Asked Answered
B

2

11

TypeScript uses compile time (static) duck typing.

I am a fan of extending primitive types to prevent incorrect substitution. For example, I like to give a credit card number variable a credit card number type, rather than integer. I recently tried doing this in TypeScript with a pair of interfaces extending String, and found out that they freely substitute for one another (and that string substitutes for both).

I really would like to get compile time nominal typing. Any ideas?

Butterworth answered 26/3, 2014 at 23:2 Comment(4)
Do you mean nominal typing? TypeScript's type system is static, and JavaScript's runtime semantics are fundamentally weak which would conflict with a strong type system.Kathyrnkati
Yes, nominal typing is the term I needed. Edited reference to strong typing above. Thanks!Butterworth
possible duplicate of Is there a way to create nominal types in TypeScript that extend primitive types?Downswing
Lodewijk Bogaard's answer in that (younger) possible duplicate has a great explanation for the same sort of solution I proposed below. Kuba Jagoda also offers one, showing that private members can be used without risk of their member names being identical.Butterworth
B
4

I came up with one way to get stronger typing. I do not like it much. One adds a special field or method to each type that will make it incompatible with others that would be confused as ducks.

The following does not allow substitution of a Parrot for a Duck, because the Duck class has an additional method (so Parrot fails duck typing). Sparrows and Parrots are apparently substitutable in duck typing because there's nothing a parrot can do that a sparrow cannot, and vice versa. Of course, a Duck can substitute for a Parrot, because if is sounds like a parrot, it is a parrot.

Test with www.typescriptlang.org/Playground/ :

class Sparrow {
    sound = "cheep";
}
class Parrot {
    sound = "squawk";
}
class Duck {
    sound = "quack";
    swim(){
        alert("Going for a dip!");
    }
}
var parrot: Parrot = new Sparrow(); // substitutes
var sparrow: Sparrow = new Parrot(); // substitutes
var parrotTwo: Parrot = new Duck();
var duck: Duck = new Parrot(); // IDE & compiler error

alert("Parrot says "+parrot.sound+" and sparrow says "+sparrow.sound+", and 2nd parrot says "+parrotTwo.sound);
alert("A duck says "+duck.sound);

More practically, I would do this (which works in my IDE but not in the playground):

interface RawUri extends String {
    rawUri;
}

interface EncodedUri extends String {
    encodedUri;
}

var e: EncodedUri = new RawUri(); // IDE & compiler error
var r: RawUri = new EncodedUri(); // IDE & compiler error

Distasteful, and an opportunity for another interface to accidentally use the same field name. I suppose one could add a random element to the anti-duck member.

Butterworth answered 26/3, 2014 at 23:2 Comment(0)
F
1

Please consider the following question:

Atomic type discrimination (nominal atomic types) in TypeScript

With it's example:

export type Kilos<T> = T & { readonly discriminator: unique symbol };
export type Pounds<T> = T & { readonly discriminator: unique symbol };

export interface MetricWeight {
    value: Kilos<number>
}

export interface ImperialWeight {
    value: Pounds<number>
}

const wm: MetricWeight = { value: 0 as Kilos<number> }
const wi: ImperialWeight = { value: 0 as Pounds<number> }

wm.value = wi.value;                  // Gives compiler error
wi.value = wi.value * 2;              // Gives compiler error
wm.value = wi.value * 2;              // Gives compiler error
const we: MetricWeight = { value: 0 } // Gives compiler error
Fallfish answered 23/5, 2019 at 8:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.