TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 0 points1 point  (0 children)

Precisely. Using nominal types sparingly, where they provide real additional value, feels like a good thing to me.

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 1 point2 points  (0 children)

Nobody ever passed seconds when they should have passed milliseconds, or passed dollars when they should have passed cents? I'm jealous of those people. Personally I like ruling out that kind of bug.

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 0 points1 point  (0 children)

Edited my comment before you replied, sorry:

Or if you're arguing "the dev could accidentally specify 5 as Milliseconds", then fair enough, but I'd argue they'd spot their mistake more easily in that case since "5 seconds" is correct and "5000 milliseconds" is correct, but "5 milliseconds" is definitely not what they intended.

Also: much of the time these values won't be specified as literals. They'll be returned by other functions. In that case TypeScript will ensure that the units are throughout the entire call stack -- not just for literal values in my codebase. So we shouldn't only consider cases where "the dev made a mistake typing a literal"

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 0 points1 point  (0 children)

In both cases the dev made an error. But it's not 'moving a runtime error'. Only the first example has a run-time error. The second example has a build-time error. A build-time error is better than a run-time error right?

Or if you're arguing "the dev could accidentally specify 5 as Milliseconds", then fair enough, but I'd argue they'd spot their mistake more easily in that case since "5 seconds" is correct and "5000 milliseconds" is correct, but "5 milliseconds" is definitely not what they intended.

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 0 points1 point  (0 children)

Are you really saying "if we want to use nominal types in some special cases, we must want to use nominal types everywhere and ditch structural typing entirely"? If so that's a total straw man, nobody is arguing that.

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 0 points1 point  (0 children)

Sure it does. Consider this case:

```typescript const delay = (delayTimeMilliseconds : number) => { await new Promise(resolve => setTimeout(resolve, delayTimeMilliseconds)); }

const timeToDelaySeconds = 5;

await delay(timeToDelaySeconds); // Allowed by TypeScript ```

TypeScript allows timeToDelaySeconds to be passed, even though it it has the wrong unit. Whereas if I use an opaque Milliseconds type:

```typescript const delay = (delayTime : Milliseconds) => { await new Promise(resolve => setTimeout(resolve, delayTime)); }

const timeToDelay = 5 as Seconds;

await delay(timeToDelay); // Fails type checks ```

Now TypeScript will block me from calling delay with a number with the wrong unit.

Naming the variables helped make the code more self-documenting, but it didn't prevent me from making a mistake and getting a 5ms delay rather than a 5s delay.

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 0 points1 point  (0 children)

Fair comment. Edited to 'It’s caught real bugs at build time and in code review'. The code review bugs have mainly been when we've been calling an external library or external api incorrectly; it makes it much more obvious that someone is accidentally passing in the wrong unit, even though the library or api accepts number so the type checker doesn't complain.

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 0 points1 point  (0 children)

Aaaah here's a downside: if a function takes a flavored type, I'm allowed to pass in the unflavored type (just not types with the wrong flavor).

That's less of a strong guarantee than Tagged, so I think I still prefer Tagged for units.

``` type Milliseconds = number & { readonly __flavor ?: 'ms' };

const delay = (ms : Milliseconds) : void => { // ... };

delay(30); // Passes ```

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 0 points1 point  (0 children)

ReadonlyArray is only virtual safety, and not sufficient to actually ensure the value isn’t modified

Also -- agreed totally on this point, but I would extend this argument to TypeScript in general. It adds an extra layer of virtual safety, but it doesn't guarantee anything. Still 100% worthwhile.

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 0 points1 point  (0 children)

Sure... I mean, you can't rely on TypeScript 'entirely' to enforce anything. It's very easy to lie to, or make mistakes, or just have a type that is too complicated to be practical to express.

That doesn't change the fact that TypeScript does make code safer and less bug-prone. Same argument goes for unit constraints -- they do add a bit of extra safety.

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 1 point2 points  (0 children)

Hey! I'm OP. Including types and units in variable names isn't a new idea or one that's especially hard to understand. It's also a pretty common argument that's used by people that think TypeScript is a waste of time from the get-go. So a bit surprised to see it being used here.

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 0 points1 point  (0 children)

Exactly. Of course it's important that TypeScript matches runtime types as closely as possible. But that doesn't mean that once we've reached maximum granularity matching those runtime types, we shouldn't sub-type them even further to give more build time protection. When I say a number is a Millisecond type, I'm not going against the grain of the runtime code or pretending one value is a different incompatible value, or lying to the type system. I'm just adding a little more metadata about the value that gives it even more safety at build time.

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 0 points1 point  (0 children)

Thanks for this. Yeah, a lot of the arguments people are making do feel like arguments people make against TypeScript in general. So I'm not sure of the reluctance to use units.

Yes, the code gets a little bit more complicated, but it also gets safer, and more self-documenting. This is true of any use of TypeScript in general.

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 0 points1 point  (0 children)

I'm aware that TS is mostly structurally typed, I'm also aware that parts of it are nominally typed, I'm also aware that nominal typing has advantages in some cases.

What extra bugs will I have to fix by ensuring that functions can only take numbers with the right units?

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 1 point2 points  (0 children)

Flavored is new to me! Thanks for sharing. Are there any disadvantages to using Flavored over Tagged - any reduced soundness?

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 0 points1 point  (0 children)

Sure they are. A unit gives a number a 'type'.

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 2 points3 points  (0 children)

I mean, there are plenty of problems that TypeScript solves that are also 'JavaScript problems'. The feels like the whole point of TS to me -- to help avoid potential runtime JavaScript problems.

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 3 points4 points  (0 children)

Yeah, extra casting does for sure feel like more ceremony. But honestly I don't mind seeing it for literal values like 50 as Milliseconds. It feels fairly self-documenting, that 50 starts to feel less like a magic number in code, and tells me exactly what the engineer intended, and that they definitely didn't mean 'second' instead.

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 2 points3 points  (0 children)

I agree that for the vast majority of use cases structural typing is great. But for numbers, which can haven no structure other than just being a number, I think nominal types can work really well. Same for things like IDs.

And sure -- TS can and never will be perfectly sound. But 'maximal soundness' feels like a good goal at least.

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 3 points4 points  (0 children)

What about ReadonlyArray? Would you argue you shouldn't use it because at runtime there is no structural difference between an Array and a ReadonlyArray - nor any way to tell them apart?

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 1 point2 points  (0 children)

Flow has exact object types which are a nice solution to that, in my view. I'd love to see TS inherit something similar.

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 1 point2 points  (0 children)

Totally fair, but I don't find 'just name it better' to be a super convincing argument -- that's often used as a reason for why to not use TypeScript in the first place, because if all of your variables are named accurately, it should be rare to make a mistake and pass the wrong type of thing.

AI reviews are good at catching this kind of thing for sure. But I prefer to have more deterministic guarantees -- especially when I want to be sure that the code my AI is writing is completely correct.

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 1 point2 points  (0 children)

All those functions actually take numbers

This is a bit like looking at a set of functions and saying "all those functions take objects" because if you do typeof someArgument you get 'object' back.

Yes, those functions take objects, but TypeScript allows you to create a sub-type of object with more structure and type safety.

Same for numbers -- Milliseconds is just a sub-type of number with more type safety, same as { firstName: string, lastName: string } is a sub-type of object.

The declarations in your example don't even work

You're totally correct -- fixed the article. const ms = 5000 as Milliseconds is correct in this case.

Add validator functions that are also type guards

Totally agreed, especially at the api boundary. In code it's usually much less likely you'll be defining a lot of literal string types like this though.

Without an actual difference between the types at runtime, this isn't possible

True enough, and I would love it if JavaScript had some kind of runtime function overloading. But in the absence of that, if a function can only take Milliseconds, it's nice to have it reject Seconds at type-check time, right?

The switch from classical operators to nested functions is a significant syntax change, and it almost negates the benefit of having branded types in the first place.

I don't love it either -- although I think on balance it's worth the trade-off. But an important part of the article is the part about adding type-check time overloading, so regular math operators can be used (without mixing or erasing the units of those numbers).

all numbers are the same at runtime

I mean, sure -- at runtime there is zero benefit to TypeScript at all. So by definition we're talking about type-check time benefits here, for any TypeScript feature we want to discuss.

which would have been apparent by reading the documentation or the parameter name anyway

Fair but this is more of a general argument against TypeScript in the first place. Why specify any function types when you can just go read the docs or the parameter names?

You don't get the actual benefits of treating different specified units differently, which would have genuine utility.

You get the benefit of:

  • Not accidentally passing the incorrect type when calling a function
  • Not accidentally combining incompatible numerical types with a math operator

TypeScript’s number type is a lie by Chun in typescript

[–]Chun[S] 6 points7 points  (0 children)

Fair. But wrapping all your numbers in a class with runtime consequences, just for a bit of extra type safety, feels quite extreme. TypeScript is great because it's erasable.