Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

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.


Oh wow. Was there a “stomach sank to the floor” sudden feeling upon discovery?


Not really. The whole thing was kept together with tape and silly string anyway, and I bet it still is now, more than 5 years on.


how did that work out in the end?


I mean, no problem ever occurred. Things just accidentally worked even though they shouldn't, and that isn't a bug.

The introduction of the additional types everywhere did turn into massive headaches based around dependency management and versioning.

So overall, I was personally disappointed in the results, but nevertheless happy to work with less "icky" feeling code.


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.


You can kind of do it:

  interface RobotName {
    value: string;
    type: "RobotName";
  }
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.


Discriminators are super power powerful. Love them. But they can be just so heavy for things.


Oh cool. So you basically lie about the runtime structure of the type, which works out fine during type checking at compilation.


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


“nominal typing” thanks for the link and terminology!


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.

[0]: https://ocaml.org/docs/labels


In haskell this is done with newtype. Another example would be using km and miles, and making sure they are not mixed.

Here there is an interesting article about doing this in typescript (not affiliated)

https://kubyshkin.name/posts/newtype-in-typescript/


type robotname = string & {_robotname_marker:true}

or some variation. The marker does not actually need to exist.


The simplest way I know of to make a discriminated number type in TypeScript is:

    declare const isRobotID: unique symbol;
    type RobotID = number & { [isRobotID]: true };
and now you can cast a number to a RobotID and back.


type RobotName = string;

function foo(r: RobotName) {}

?


Yep. But that function will accept

type RobotUuid = string;

const bar: RobotUuid = “abc…”;

foo(bar);

This is what duck typing is and specifically my curiosity about being able to de-duck on demand.


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.


> 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.

Yes, and that’s roughly what TypeScript is: a linter that everyone on your team is running.


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?


I don't think it is possible in Typescript, but in other languages you can do assertations about custom types. Like

assert istype(whatever, MyCustomType)

Which would throw an exception if whatever is not a "MyCustomType" at runtime.


In TypeScript any type that's structurally the same is considered equal, type just gives it a different name.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: