Stricter TypeScript >, <, and ===

Jesse Warden - Oct 17 '23 - - Dev Community

TypeScript needs a new feature for Type aliases to allow stricter comparison for >, <, and ===. Possibly for Interface as well, but I don’t think the demographic that utilizes Interface would care.

When creating Objects, JavaScript needs a valueOf method so it knows what to do with > and <.

2 > 1 // true, works with number
{} > {} // false, JavaScript doesn't know
{ valueOf: () => 2} > { valueOf: () => 1 } // true
Enter fullscreen mode Exit fullscreen mode

When you’re creating type aliases, you don’t create “methods”; it’s just a Record; it’s data. For example, if you play with the JavaScript Stage 2 Record proposal, this generates a runtime type error that you cannot convert Records to Numbers:

const proposal1 = #{ name: 'cow' }
const proposal2 = #{ name: 'sup' }
console.log(proposal1 > proposal2)
Enter fullscreen mode Exit fullscreen mode

Whereas, if you used regular Objects in our previous example, it works fine.

If you change an Array.sort method’s compare function or an Array.filter from using a number to an Object, the code will still run, the results will be different because comparing 2 Objects with > and < always results in false. When using TypeScript like I did where you change a number to a type alias, any place in the code that uses those 2 Array methods will “still work”. Thankfully I had unit and acceptance tests that started failing despite TypeScript saying “all is well”.

The simplest way to fix this for > and < is to simply get Record’s to Stage 3:

https://github.com/tc39/proposal-record-tuple

… which will in turn get TypeScript to implement them. Based on the possible new Dictionary type (think TypeScript Dictionary === JavaScript Record, TypeScript Record === JavaScript Object):

https://github.com/microsoft/TypeScript/issues/49243

… would mean I could convert this:

type Grant = { year: number }
Enter fullscreen mode Exit fullscreen mode

To this and get the type safety for > and < operators:

type Grant = #{ year: number }
Enter fullscreen mode Exit fullscreen mode

An example, if we take the code we have now, sorting an Array of numbers:

someGrantArray.sort( a:number, b:number ) => {
 if(a > b) {
  return -1
 } else if(a < b) {
 return 1
 } else {
 return 0
 }
}
Enter fullscreen mode Exit fullscreen mode

Then later refactor it to take a type alias:

type Grant = { year: number }
Enter fullscreen mode Exit fullscreen mode

Then fix the compiler errors:

someGrantArray.sort( a:Grant, b:Grant ) => {
  if(a > b) {
    return -1
  } else if(a < b) {
    return 1
  } else {
    return 0
  }
}
Enter fullscreen mode Exit fullscreen mode

… that compiles, but results in always return 1. If you used Records, it’d at least fail at runtime. One point of TypeScript is to help prevent runtime exceptions via type safety. If you used the suggested Dictionary class in TypeScript, which like Records does not allow methods on the Records, which in turn means you cannot utilize a valueOf which in turn means TypeScript “knows” you can’t convert a Record to a number, which means you’d get a compiler error. That’d be awesome.

However, that doesn’t solve ===. If you have JavaScript comparing numbers:

const fund:number = 2019
someGrants.filter ( (year:number)=> year === fund )
Enter fullscreen mode Exit fullscreen mode

Then refactor to use a type alias:

const fund:Grant = { year: 2019 }
someGrants.filter ( (year:Grant) => year === fund )
Enter fullscreen mode Exit fullscreen mode

That code compiles, but is NOT what you want; the intent was “filter by year” not “do these Objects equal each other”. I recognize that equality has it’s own bag of complexity.

For example, using Discriminated Unions in TypeScript:

a = Ok(1)
b = Ok(1)
a == b // false
Enter fullscreen mode Exit fullscreen mode

Whereas, using them in Elm:

a = Ok 1
b = Ok 1
a == b -- True
Enter fullscreen mode Exit fullscreen mode

The TypeScript way makes sense to those who know JavaScript, or are used to an imperative/OOP language. To FP developers who are used to Sum types, the Elm way makes sense. The imperative/OOP developers think “it’s a unique instance, of course those 2 don’t equal each other, they’re different things, different memory addresses in RAM”. The FP devs are “Why does 1 not equal 1? If I write 1 != 1 in JavaScript, I get false, and if I write 1 == 1, I get true… so what’s going on?”

That’s why a few of the FP libraries that support Discriminated Unions will encourage the use of pattern matching, like “match” rather than rely on operators that weren’t built for that style:

https://gcanti.github.io/fp-ts/modules/Either.ts.html

Fine, so FP devs can hide behind match’s they have to build manually; but what about imperative/OOP devs? Maybe a ==== operator or perhaps a compiler setting for “strict comparison” that flags the compiler to either look for valueOf on the interface, or maybe just a linting rule to prevent the use of == & === entirely?

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .