Hacker News new | past | comments | ask | show | jobs | submit login
Making sense of TypeScript using set theory (thoughtspile.tech)
140 points by hrafnstrom 11 days ago | hide | past | favorite | 91 comments





The difference between `any` and `unknown` is that `any` is an escape hatch from the type system. `any` can be used anywhere and will satisfy any type constraint. `any` is how the developer says to the type system "trust me, I know what I'm doing, don't worry about this particular value." Unknown, on the other hand, is for untyped code from an imported JS library, JSON data received from the network that may or may not match the API spec, etc. Code that does have a type, but that type needs to be confirmed at runtime and can be narrowed through type guards.

> I know what I'm doing

`any` can also be an indicator of the exact opposite.


It certainly is for me :)

I went through my code base and found one use of `any` that was worth keeping around, the rest could be changed to `unknown` and better code resulted from it.

That’s a convenient way of thinking about it. A more logically correct way of thinking about it is:

“any” can be any possible type

“unknown” is a specific stand-in type. It does not represent all types but is a subset of all types, which allows you to narrow its definition from nothing.

Unknown in practice is closer to a generic in terms of usage and semantics than “any”. It’s like a generic generic except it’s not useful until you direct it. It basically lets you kick the can down the road in your code of having to make a type decision, which is very useful for writing composable and uncoupled code.


I think "unknown" is more than that! Basically it's a way to say you don't care about the type, as in Record<string, unknown> or P extends Promise<unknown>

This is technically correct, but I suspect it's pretty rare that you would choose "unknown" over a generic parameter. Even if you're writing an algorithm that doesn't care about the data type, it's still probably going to be used by application code that does care, and you'll want to support that need by passing the type through, otherwise the application will be forced to downcast.

I often use it to infer one generic parameter:

type Output<Fn> = Fn extends ((a: unknown) => infer Out) ? Out : never;


How is unknown better than any in this case? If you do not care about the data type, wouldn’t you say that “any” data type is acceptable? If you say “unknown” that means that there is possibly some data type that would break your function.

The problem with using any where you don't care about the type is that someone can come along and make unchecked assumptions about what type it actually is. Unknown is spicier as it'll prevent developers from assuming it's a string or object or something.

That makes some sense to me.

I’m not a TypeScript dev so I apologize for the stupid question. Is there any functional difference between the two? I.e. is there a case where using Unknown instead of Any will result in some sort of “compile” time error opposed to a runtime?


Yes. `any` really does turn off all type checking. You see that offhand mentioned in this article talking about some of the paradoxes of `any`. `unknown` is still type checked and is arguably "merciless" type-checked that to do much of anything with an `unknown` you have to check for a more specific type first or the compiler returns an error that what you are doing isn't known to be valid for `unknown`.

Some of that happens "automatically" at this point in the large number of ways that types can now be narrowed implicitly in Typescript (type guards [library functions], type asserts, the `typeof` runtime operator, the `in` runtime operator [as of recently], etc), so it can feel like `unknown`/`any` are the same up to a point, that point being where the runtime type is trivially known based on if statements and library functions around your use of the type.

(Fun fact: `any` predates most type narrowing and `unknown` by several major Typescript versions. So `any` beyond just being the final "escape hatch" from type checking is also something of a legacy tool.)


Yes - trying to access a property or method on something typed as unknown will (almost?) always result in a compile time error because it's unknown whether the value has that property or not.

https://www.typescriptlang.org/play?#code/GYVwdgxgLglg9mABAd...


always, because "unknown" includes null / undefined, which don't allow property access at all.

Almost every case where you use the value results in an error with unknown, but not with any:

  let danger: any;
  // These all compile:
  danger(), 9 / danger, danger.access, danger.map(x => x \* 2)

  let safe: unknown;
  // These all explode in TS:
  safe(), 9 / safe, safe.access, safe.map(x => x \* 2)

Saying a value is "unknown" means making no assumptions about the value. It might be a null, a number, a function, you don't care because you aren't going to do anything with this value. If you call() it, or read.some.property, TS complains because you're making assumptions about the object that TS did not ensure.

But you have to care about the type with unknown? For instance calling this on an unknown

    obj.value
Will return a type error that value doesn’t exist on unknown. While calling it on an ant is valid typing

> But you have to care about the type with unknown?

It's better to say `unknown` is a promise that you won't care about the type. When you write `obj.value`, you're caring about the type. TypeScript sees that and says "hey, you promised you wouldn't care about this type."

> While calling it on an ant is valid typing

It's still invalid, same as if you'd used `unknown`. It just suppresses the type error. The presence of `any` in a codebase is a very real risk. The only reason it exists is to allow incremental porting from JavaScript to TypeScript.


Here, you expect obj to have a property called "value", so I'd say you do care about the type of obj. A valid type would be obj: { value: blah }

`any` should really be called `every` to match the semantics, it says "this value is an instance of every type, and therefore you can do anything with it".

And you might read that and thing "but that's crazy, its not possible for a value to be an instance of everything!" and you'd be right - the `any` type in TS is in fact crazy and you should not use it.


The intersection of all typescript types is `never`. `any` is approximately equivalent to `unknown` in contravariant positions and `never` in covariant ones.

I might be missing something, but isn't this line backwards?

> Subtype of type A is a subset of type A. Supertype is a superset. Easy.

Subtype of type A is actually a superset of type A, since it contains at least all the properties of A.

If you had (contrived example) a Dog class that inherited from an Animal class, Dog would be a subtype of Animal, but its additional properties (say, a bark() method) mean that it actually has a superset of the properties in Animal. And vice versa: Animal is a supertype of Dog, but it's a subset because it contains only the properties that Dog inherits.


Nope. This is a common confusion.

The set of properties of objects and the sets of objects themselves have a complementary relationship when it comes to union / intersection and subset / superset.

Let's define a "property" as being a predicate that is true for all elements of a set. For example, a collection of red objects has the "red" property.

A subset of a set of objects can only have the same or more properties than the set you started with. If you take a bunch of marbles of varying colors, selecting a subset of those marbles can yield a set with a new property (such as them all being red), but they are guaranteed to have the "marble" property.

A superset of objects can only have the same or fewer properties than the set you started with. Adding marbles to an existing set of marbles can only make "they are all red" become false (if it was true to begin with).

Union and intersection have the same duality; the union of two sets has the intersection of its properties, and the intersection of two sets has the union of its properties.

These results come from logic, not TypeScript.


I think that interfaces in TypeScript unfortunately contribute to that confusion. I can do

  interface Point2D {
    x: number
    y: number
  }
And then I can do

  interface Point3D extends Point2D {
    z: number
  }
And that "extends" really throws off many people because in reality the type of Point2D is implicitly

  {
    x: number
    y: number
    [anything_else in string]: unknown
  }
So Point3D actually narrows the type of Point2D even though the notation suggests that it widens it. Fortunately, the type notation is less misleading because I can do

  type Point3D = Point2D & {
    z: number
  }
Without any misleading "extensions".

It seems like it all stems from the tight coupling between types and classes in a typical OOP paradigm. When you create a class Animal you create a type Animal that should conform to specific rules, but when you instantiate that class you create an object that conforms to a narrower set of rules. E.g. an object instantiated from class Animal won't be able to woof() even though the type "Animal" easily allows it. This leads to the mental model of "extending" base class with new fields and methods. And even though it indeed extends the class Animal, it narrows the type Animal.


"Extends" does not mean it extends the type to contain more members, it means it extends instances to have more properties, thereby creating a subset or subtype.

Perhaps it is confusing that TypeScript uses terms from both set-theory (union, intersection) and OO (extends, implements). But there is no contradiction.


Right, my point is that it is confusing.

> So Point3D actually narrows the type of Point2D even though the notation suggests that it widens it.

The notation suggests that it extends the definition with additional restrictions. Which it does. Which, yes, produces a narrower scope. That’s how definitions work.


In mathematical logic, for example, saying that A is an elementary substructure of B is equivalent to saying that B is an elementary extension of A. The same with subfields and extension fields. Those concepts are opposite.

Usually in OOP you extend a class with additional implementation. Intersection of types means union of their members and vice versa. Getting into a situation where extending X gives you a sub-X seems like an unfortunate case of mixing two mental models that behave in a dual manner.


This depends on what you mean by "property".

Universal properties are preserved "downwards" i.e. in subsets (more properly, substructures). Existential properties are preserved upwards, i.e. in supersets (extensions).

For example, the property "there exists an element that equals itself when added to itself" is preserved in a superset.

If you have sentences that mix quantifiers (e.g. "for all epsilon, there is a delta ..." or even just "every element has an inverse"), then all bets are off.

The branch of mathematics where this is studied (and rigorously proven) is model theory.

edit: I guess by "property" (given the context of the discussion) you might just mean "function" or "method", in which case, yes, a function defined on a set is also defined on a subset but not necessarily vice versa, so there are in a certain sense "more" functions defined on the subset.


maybe a dumb question, but why does wikipedia say typescript is a superset of javascript?

https://en.wikipedia.org/wiki/TypeScript


Because Typescript is designed to accept any valid Javascript program, the set of valid statements in a Typescript program is a superset of the set of valid statements in a Javascript program. Typescript contains all the rules of Javascript, and then adds some more, but never in a way that contradicts the requirement that a plain Javascript program should compile, so that also means the language specification itself is a strict superset of Javascript.

> Typescript is designed to accept any valid Javascript program

That's not strictly true though. `a<b,c>(d)` is valid Javascript but Typescript treats it as a different syntactic construct[0].

[0] https://www.typescriptlang.org/play?#code/C4TwDgpgBARlC8UB2B...


With these declarations your fragment is valid in both JavaScript and TypeScript:

    class b {}
    class c {}
    const d = JSON.parse
    const a = Promise.prototype.then.bind(Promise.resolve(1))

    console.log(a<b,c>(d))
    // TS output: Promise { <state>: "pending" }
    // JS output: false false
TS playground: https://tsplay.dev/mAdeZN

It is strictly true. a<b,c>(d) is only valid JavaScript iff a, b, c, and d are values. The TypeScript expression a<b,c>(d) where b and c are types is NOT JavaScript, since JavaScript doesn't have type expressions. The statement that every JavaScript program is a valid TypeScript program does not imply the opposite - hence strict superset.

edit: there is indeed an edge case with the parentheses that throws the TypeScript parser off even if only values are involved.


See the sibling comment from oblosys for an example where the exact same code can give different results in vanilla JS vs after a tsc pass. As you can see there, the problem is that an identifier can simultaneously represent a value and a type.

This is different than JSX or hashbang, where the set of non-JS syntax cannot legally overlap with existing syntax/semantics.


I guess this is a distinction between intent and results. Typescript is intended to be a strict superset of Javascript. In practice, there are edge cases like these.

Lol, this is brilliant!

Here are 3 statements that were made in this thread:

- typescript is a superset of javascript

- superset of objects can only have the same or fewer properties

- Typescript contains all the rules of Javascript, and then adds some more

Do you see where the confusion is coming from?

> that also means the language specification itself is a strict superset of Javascript

This is where I disagree.

TS as a language has more properties than JS, but less rules. I.e. you can create Javascript language out of Typescript by adding more rules (constraints).

But TS as a spec has more rules than JS spec.

TS as a language is a superset of JS language. TS as a spec is a rough subset of JS spec.


You're making this harder than it actually is by conflating a bunch of things, comparing apples and oranges. Sets defined by type vs sets defined by lists of properties, specs, rules etc.

For types, just draw the Venn diagrams:

- The set of objects of Type A is entirely within the set of objects of Subtype of A. Subtype of A is the superset.

- the set of things that are Dogs (class or real world) is entirely within the set of things that are Animals.

- the set of things that are Javascript programs is entirely within the set of things that are Typescript programs.


> Sets defined by type vs sets defined by lists of properties, specs, rules etc.

Recursive explanations are useless: "types are just sets defined by types". That's why we are trying to define them through other means.

Also TS has structural type system, which is literally about comparing properties.


That's not what GP wrote - they wrote that there is a difference between "the set of all instances of type A" and "the set of all properties satisfied by all instances of type A". There's nothing recursive about that. And this is all completely independent of the question of whether a language is structurally or nominally typed.

(butting in)

What is recursive about the parent comment?


It's recursive in the context of "making sense of TS using set theory". If you understand sets, but not types, statements about "sets defined by types" are meaningless.

> superset of objects can only have the same or fewer properties

This is potentially misleading. A superset will include more objects and therefore may include more individual properties. But type checking is about what can be safely assumed about all members of a set, so more different objects in the set will constrain the type more.

Object is a superset of Date because the set of objects includes alle Dates but also things which are not dates and have different properties. But in the context of type checking, object is more constrained because there are fewer properties which all members of the set are guaranteed to have.

Typescript is a superset of Javascript because all Javascript programs are also Typescript programs. There is no contradiction.


> object is more constrained because there are fewer properties which all members of the set are guaranteed to have.

In my mind it's the opposite: object is less constrained, because there are fewer requirements you need to fulfill to be considered an object. Object type is very permissive.

The more constrained type is, the fewer objects it's set will contain. The more constrained the type is, the more rules it enforces. Seems intuitive, isn't it?


TypeScript is a superset of JavaScript because any JavaScript program is also a Typescript program but not vice versa. There is no contradiction.

> TS as a language is a superset of JS language. TS as a spec is a rough subset of JS spec.

This does not make any sense. A spec which fully describes TypeScript including runtime behavior would include the JavaScript spec and therefore be a superset. The TypeScript spec itself is in no way a subset of the JS spec, sine it descries features and syntax which does not exist in vanilla JavaScript.


This is how you should think about it: Spec > Language > Code.

Code is an instance of Language (and in structural typing land it can be compatible with multiple languages). Language is an instance (implementation) of Spec.

> sine it descries features and syntax which does not exist in vanilla JavaScript

That's exactly why TS spec is a subset. Take example from the article:

  type B = true extends boolean ? 1 : 0; // 1
Type B is a subset of boolean, meaning there are less objects in the world that satisfy type B.

There are less language implementations in the world that satisfy TS spec than JS spec. Every language that satisfies TS spec also satisfies JS spec. TS spec adds more constraints (requirements) to language implementation compared to JS spec, it is more strict, therefore it's a subset.


So you are saying the implementation of the Typescript language are a subset of all JavaScript implementations? I guess that is true in a certain sense. But this does not contradict the fact that TypeScript as a language is a superset of JavaScript - rather it follows logically.

(Although in reality the Typescript implementation is a preprocessor, intended to use together with a regular JavaScript engine.)


Kind of. The language gets confusing because we use subset and superset next to each other, but we put different meaning on them.

{foo: number, bar: string} as type is a subset of both {foo: number} and {bar: string}. It requires it's members to have both properties. You can construct this type using intersection:

  type Foo = {foo: number}
  type Bar = {bar: string}
  type FooBar = foo & bar

{foo: 1, bar: 'hello'} as object is a superset of {foo: 1} and {bar: 'hello'}. It contains both properties. We can construct this object using union (I use pipe instead of spread to illustrate the idea):

  let foo = {foo: 1}
  let bar = {bar: 'hello'}
  let foobar = foo | bar

When I'm saying TS is a superset of JS, I mean it in the object sense. It has all the properties of JS, and some more. All JS programs are also TS programs, but not vice versa. There's more JS programs in the world than TS programs.

When I'm saying TS spec is a subset of JS spec, I mean it in a type sense. Language is an instance of a spec. TS spec has all of the requirements of JS spec, and some more. All TS implementations (languages) will contain JS implementations, but not vice versa. There's more JS implementations in the world, than TS implementations (assuming you can build TS as compiler/interpreter instead of transpiler).


> All JS programs are also TS programs, but not vice versa. There's more JS programs in the world than TS programs.

I think you are confusing terminology, because those two statements are contradictory.


You're absolutely right, last statement should be inverted. I think meant to say something else, but at this point I don't remember what it was.

In any case, the TS spec is a superset of the JS spec, since it incorporates the JS spec. If you don't consider the JS spec as part of the TS spec then they are disjoint sets. But the TS spec cannot be a subset of the JS spec since that would mean it removed some parts of the JS spec, which is not the case.

You are right, the statements are contradictory. It's strange to see how nobody questions marketing buzzwords from Microsoft.

What "marketing buzzwords" are you referring to?

Because all valid JS code is also valid TS code (considering implicit any). Or, put another way, the set of all valid JS programs is a subset of all valid TS programs.

In your example with marbles, the superset has the property of every element being comprised of a set of colors, let's say {black, red, blue}.

The subset is {red}.

Can't every possible property we can conceive of a subset be constructed in a similar fashion such that the superset contains at least the same number of elements and never less?


> Can't every possible property we can conceive of a subset be constructed in a similar fashion such that the superset contains at least the same number of elements and never less?

What do you mean? A superset always contains at least the same number of elements as a subset, by definition. A superset generally has fewer or at least weaker properties; "colour is one of A, B or C" is a weaker property than "colour is A", and there's no real difference between that and saying the superset doesn't have the property at all (I could say "my subset consists of shiny marbles; my subset has the property that they're shiny and my superset does not have that property", or I could say "my subset has the property that shininess is true, and my superset has the property that shininess is true or false", and there isn't really any difference once you get down to the set-theoretic level - a property is just a boolean-valued function). The only superset that has exactly the same properties as the set itself is the set itself (since "is a member of the subset" is a valid property).


Ok, how about I define a new property that my superset contains a 'rainbow' of colors. The subset has only one of the colors, it doesn't contain the rainbow property. The original post claimed that the superset always has the same or fewer properties. I was questioning whether this was true.

You've mentioned that it is generally true, which seems like a better way to describe the concept.

-- edit: I understand now, in my example above the subset must contain a property by which it could be identified. The original rainbow property is lost but a new one is gained.


> Ok, how about I define a new property that my superset contains a 'rainbow' of colors. The subset has only one of the colors, it doesn't contain the rainbow property.

You're mixing levels. The property is something satisfied by members of the set, not by the set as a whole.


The way "subset of properties" is being used here is loose and that's where the confusion is arising from. If we want to be a bit more rigorous:

* Define O as a set of possible (finite) objects

* Define properties as functions P: O -> {0, 1}

* A given property then is a function of the form p(o) = 1 for all o in O that satisfy the property p.

You can use property functions to generate subsets of O ({ o for o in O where p(o) = 1 }). The intersection of these generated subsets of multiple property functions will be strict subsets (strictly "smaller" as O is finite) of O as long as at least one property function is not of the form P(o) = 1 for * in O. Generally assuming that each property function generates strict subsets, applying more properties derives a smaller intersection set. That's all.


Thanks for that, it makes sense using your definition. I'm curious as to whether you agree with the parent comment that the subset could contain 'more' properties than the superset.

My understanding from your stricter definition is that (by definition) all potential properties (o for o in O where p(o) = 1) must be contained within O.

If the answer is that the superset O has fewer properties where every p(o) = 1 then I agree, and I was overthinking it


> If the answer is that the superset O has fewer properties where every p(o) = 1 then I agree, and I was overthinking it

It's just this honestly. A superset will generally have fewer properties which apply across the whole set.


You're making the same mistake as I sometimes do, thinking in terms of "object shape" or "functionality". Here, I use "subset" in a sense of "all values that belong to Sub also belong to Super", in line with a set-theoretic view of types in the post. Also see subsets on wiki: https://en.wikipedia.org/wiki/Subset

Try this one: A is subset of B iff all the values that belong to A also belong to B. In your example (and in real life), every dog is an animal, so dogs are a subset of animals.


Types are not features, they are constraints.

"at least all the properties of A" means "all of the constraints of A and some more". Subtype of A is more narrow than A, even if it means that object of subtype A has to have more fields.


> Subtype of type A is a subset of type A. Supertype is a superset. Easy.

I think it is correct.

Example:

type A = { a: number }

type B = { a: number, b: number }

B is not a superset of A, it's a subset and subtype of A. You can use B every time you want to use A.

In other words, of all types that can be assigned to A or used in functions and types expecting an A, B is one of those subsets.

type C = { a: number, c: number }

is another subtype of A.

A "subtype" C can be substituted in place of its "supertype" A.

Both C and B are subsets of A, in other words: in the infinite set of values of type A, some can be grouped in a type B or C. C itself is a subtype of A.


In your case, dogs are only those animals that bark. That's a subset of animal. Think of it this way: can you think of an object that satisfies (is-a) Dog, but not Animal? No. Can you think of an object that satisfies Animal, but not Dog? Yes. So Dog ⊂ Animal.

hbrn has it right calling those properties constraints, not features. With anything but a sealed class, the properties that are not present are unspecified rather than missing.

And indeed sealed classes feel very "un-set-theoretic" to me.


I think what’s confusing here is that there are two mental models that use the word “type” in an opposing way: if you think of types in terms of set theory, then “subtype of type A is a subset of type A”. But if you think of types in terms of a class hierarchy (like in Java), then by saying “subtype of type A is a superset of type A” you actually mean “an inherited type (class) that is further down in the class hierarchy has the same or more properties than the parent type (class) that is further up”.

So I actually would agree that your original statement is valid, assuming that you refer to “types” as “classes” in the sense of Java class hierarchies.


> 4. A extends B ... can be read as "A is subset of B".

I agree, it is unnatural to think of a subset as "extending" the set it is part of.


Yeah, it's a common way of telling people how to read "A extends B", but it's not quite right. The two relationships are related (no pun intended) but they aren't the same relationship. `A extends B` means (in the OO world) that A adds fields, methods, or changes the behavior of methods to B. `A is a subset of B` means that all As are Bs, but some Bs may not be As.

It happens that `A extends B` introduces a subset relation between them, but it's not correct to describe it as the same as the subset relation.


> Why does 0 | 1 extends 0 ? true : false evaluate to false?

Because 'extends' really means 'is assignable to'.

I feel like most of the questions from the post might be answered fairly easily by using reasoning of 'is assignable to'.

Things assignable to A&B are things assignable to both A and B.

Things assignable to A|B are things assignable to A or to B.

'never' means a type that nothing is assignable to and is assignable to everything (bit weird because it usually doesn't have any values but you can create value of type never with the use of 'as').

'unknown' means a type that everything can be assigned to but it's not assignable to anything (except itself and any).

'any' means a type that everything is assignable to and that is assignable to everything (except never).

Ok. Ok. {} types is bit weird way to denote interfaces. :-)


Fair enough, "is assignable to" is another synonym for "is subset of". I find it much easier to reason about things I can visualize, like sets, which is why I love my set interpretation.

Edit: besides, it's quite unintuitve that "never" is assignable to anything. How can never be something?


> Fair enough, "is assignable to" is another synonym for "is subset of".

Only if you're happy to conflate members and sets, which is pretty confusing IMO. If I write a = b I don't generally think of b as being a set, even though in some sense it is; "b is an expression with this type" is a much more natural way of thinking to me.


> it's quite unintuitve that "never" is assignable to anything

Yeah, at first I was mislead by this.

Initially I thought never is opposite of any while, it turns out, it's opposite of unknown. And any is just super weird, dynamic wildcard thing that is as large or as small as needed for any given operation (but not as small as never).


It's harder for me to make that connection but thank you for your write up. It's really great.

Because "never" is an empty set and empty set is a subset of all sets.

Exactly, that's why I wrote the article =) Makes perfect sense in "set world", but not in "common sense" based on your feeling of the word "never"

Yeah, `extends` seems like a misnomer that confuses people.

> unknown is the set of all JS values. any is a paradoxical set that includes everything, but might also be empty.

Not really a paradox.

- `unknown` is the intersection (&) of all types, including an internal one that has no shape or characteristics. `unknown | {int: number}` should be `{int: number}`. `unknown & T` is always `unknown`, any characteristic T has will be discarded by the intersection.

- `any` is the union (|) of all types, including that internal one that can be anything, and yes, `unknown` too. `(any & {int: number})` should be `{int: number}`. also, `const x: any = 5 as unknown`. Union with `any` should always produce `any`.


I think you have switched union and intersection. ’Unknown’ is a type which can be anything, so it is the union of all types. ‘Never’ is the empty set of types.

‘Any’ does not really fit into a set theoretical model because it can be assigned to anything and anything can be assigned to it, so it is both a supertype of anything and a subtype of anything. Basically it just disables type checking.


I'm afraid you're confusing something!

The intersection of all types is never, because, say, number and string don't intersect. The union of all types is unknown.

any doesn't show set-like properties and yields ternary logic in clauses like any extends T, which makes it a paradox.


Unrelated set theory thing: They say Typescript is a "superset" of Javascript, but plenty of valid JS code won't work in TS. For example, `let foo = 4; foo = "4";`

That isn't what "TS is a superset of JavaScript" means. That phrase means "TS syntax is a superset of JavaScript syntax". Your example is syntactically valid in TS -- it just fails a type checking

I see what you're saying about syntax and type-checking being separate stages, but they're both compile-time (or do we call it transpile-time?) errors enforced by the language. Either way, I can't copy-paste working JS code into TS, which is what matters.

There's probably another "asterisk" for this, but the other thing is how TS in Node by default won't allow `require` (vanilla NodeJS), only `import` (ESM).


Importing with `require` is CommonJS, not part of JavaScript’s spec. CommonJS is a Node-only thing so supporting it isn’t required in a JS superset

Having set types like this and refining them smaller is something I wish Haskell would learn from Typescript, especially the automatic inference side. I wonder if it would help with linear types? Are there any proposals? I know there are type level naturals in the type system, but this is more like wanting to deconstruct existing types like Int or String into subset types.

e.g.,

foo :: Int -> 3::Int | 4::Int

foo 4 = 4

foo _ = 3

bar :: 3::Int | 4::Int -> Bool

bar 4 = True

bar 3 = False

-- (bar 12) is a compiler error, and no need to handle other patterns

baz :: 3::Int | 2::Int -> Bool

bar 3 = True

bar x = False -- Type of x is 2::Int


> Having set types like this and refining them smaller is something I wish Haskell would learn from Typescript, especially the automatic inference side

Haskell has far better type inference than Typescript in large part because it doesn't have subtyping.

There are libraries for open records and sums (e.g. https://hackage.haskell.org/package/vinyl) but they're almost always the wrong choice.


> Haskell has far better type inference than Typescript in large part because it doesn't have subtyping.

That's a bit like saying "Go has a faster compiler because the language is so simple". It's true, but there's a genuine tradeoff here.

Haskellers have convinced themselves that subtypes are not worth it - and they may be right in many cases - but subtyping is a quite natural way of modeling many concepts (for example: mathematical expressions). Some of that can be recovered by typeclasses, existential types and other machinery (or you could go as far as dependent types), but that seems all more cumbersome and hard to understand than the way subtyping works in languages that support it out of the box.


That's my point. Haskell hasn't failed to "learn" from Typescript, it's picked a different point on the same frontier. There's no strictly improved structurally typed Haskell out there waiting to be written; there are fundamental obstacles in the way.

LiquidHaskell[1] implements refinement types for Haskell. Would that do what you'd like?

[1] https://ucsd-progsys.github.io/liquidhaskell/


Great article!

Not entirely related to the post, but what app did you use to create your diagrams? They look fantastic.


Thanks!

I use excalidraw: https://excalidraw.com/

I think most web-dev bloggers use it, makes me feel very boring




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

Search: