I think I like the duck typing in TypeScript more than I dislike it.
But I still wish I could say “this function accepts a type called RobotName. It’s a string, but so is RobotUuid, and we don’t want that. So only accept, strictly, objects typed as RobotName.”
Way back in the 1990s I worked in a place where we had to use Ada and follow a strict style guide. The guide prohibited using raw numeric types (such as "unsigned int" or "double" in C-like languages) and required creating a new type (https://en.wikibooks.org/wiki/Ada_Programming/Type_System#De...) for each different quantity, such as temperature or speed. Then you couldn't assign a speed value to a temperature variable, or compare them to each other or do arithmetic, unless you specifically define what the operators mean.
It felt like a lot of busywork, but it did prevent some classes of bugs.
In the mid-2010s I worked at a company that switched from using raw ints to refer to database rows to using (in C# terms) "Id<FooTable>".
Across the entire codebase, we discovered an entire class of bugs that only never cause any issues because all the important rows in all the important tables had Id 1 (e.g. Currency 1 was USD and Country 1 was USA) - so in a few places where the ints got mixed up, the correct row was still accidentally looked up in the wrong table).
Something I learned long ago, but occasionally disregard to my peril: if you see something that looks like a bug, but the code/system still works, stop and figure out why it works.
It's very easy to mentally shrug and move on, but more often than not it comes back to bite you; maybe it's a code path that's rarely triggered, e.g.
I have the same policy for similar circumstances. Sometimes, something isn't working, and I make a change that fixes it, but I didn't expect that change to fix it. Almost always it's worth me investigating why it's now working, because it indicates a deeper problem that would come back to bite me.
Many C++ code bases still do this pervasively, it is a common practice to improve robustness. I don't know what it is like in Ada but C++ metaprogramming makes this not too onerous.
Or if you don't want to make an extra wrapper around every object:
interface RobotName {
_phantomType: "RobotName";
}
function makeRobotName(name: string): RobotName {
return name as any as RobotName;
}
function getRobotName(robotName: RobotName): string {
return robotName as any as string;
}
Presumably V8 is smart enough to inline the wrapper functions.
The closest way to get nominal typing in TypeScript is with type branding. I haven’t actually used this small library, but it illustrates the idea: https://github.com/kourge/ts-brand
This is actually one of the nice features about OCaml that comes in handy when you have a function that takes two arguments of the same type but don't necessarily need to make a whole new type for each of them. They are called Labelled Arguments [0] and when you call the function, the label has to be the same. I've found that using it can clean up code because it ensures that variables share the name across the codebase as well as making sure arguments don't get mixed up.
Yeah just means you have to rely on linting and your ide to catch those errors. And hopefully the rest of your team does the same thing.
Tho I suppose if it is really important you can put an assert there but I'm not familiar with that wrt typescript, maybe the transpiler would kill that?
I've done the occasional type checking in that way in similar languages, it is kind of self documenting too. 90% of the time duck typing is what you want.
If robot0 name is AAA and uid Is BBB, and robot 1 name is BBB and uid AAA, and you call the function checkRobot('AAA'), what sort of assert exactly could differentiate between a name and a uid?
But I still wish I could say “this function accepts a type called RobotName. It’s a string, but so is RobotUuid, and we don’t want that. So only accept, strictly, objects typed as RobotName.”