Settings

Theme

HypeScript: Simplified TypeScript type system in TypeScript's own type system

github.com

202 points by kerneloops 4 years ago · 83 comments

Reader

efortis 4 years ago

“I'm not about to start using it for real projects, it's really just a thought experiment about how nice JavaScript could be with an alternative syntax.”

- jashkenas 2009

https://news.ycombinator.com/item?id=1014225

pfraze 4 years ago

This is epic. There’s a parser in there.

Edit: this is explained a bit here [1]. It uses Template Literal Types[2].

1 https://blog.joshuakgoldberg.com/type-system-game-engines/

2 https://www.typescriptlang.org/docs/handbook/2/template-lite...

staticassertion 4 years ago

I'm impressed by how readable the live demo is.

I have to say, the fact that you can do something so... obscene, but in a way that's actually surprisingly readable, speaks highly of Typescript's type system and design.

quickthrower2 4 years ago

Please tell me this means you can do dependent types! I have no idea how this works, seems like magic and seems like anything is possible.

Maybe "dependent-but-stringly typed" is possible?

  • dwohnitmok 4 years ago

    TypeScript has a very limited form of dependent typing made available through `typeof`, but it does not have dependent typing as e.g. Idris does. However, in general, these sorts of demonstrations do nothing to show that a language can be dependently typed. These sorts of demonstrations show that a type system is Turing complete. However, a type system can be Turing complete without being dependent. For example, type-level integers might be completely different from run-time integers with no way to things from the latter to the former.

    • tcard 4 years ago

      Sure, but plenty of type systems are Turing complete. What makes this demo impressive is the (unique?) feature of string literal types and template literal types, which lets you operate on text (like, actual text, with no weird type-level encodings).

      • Quekid5 4 years ago

        Well, certainly string literal types have been around in GHC Haskell for quite a while (7.10.x). They've also been a thing in Scala 2.12.x although the 'implementation' was some sort of weird type checker 'hack'. I believe Scala 3 supports them natively.

        I can't speak to the ergonomics of actually implementing anything which uses them internally since I've never really had much use for them outside using the surface-level API of a couple of libraries.

noduerme 4 years ago

This is insanely cool voodoo. But... I may be lacking in imagination here... I'm trying to think of how I'd actually use it. Maybe it'll come to me in the middle of the night 8)

I do really wish TS had actual inbuilt reflective type checking along the lines of this: https://github.com/Hookyns/tst-reflect ...I can hazily see some kind of monstrosity that could come from linking these things together hah

  • dfee 4 years ago

    This linked project is very cool! I wonder if this path is expressly a non-goal of the TypeScript team though…?

    • brundolf 4 years ago

      Yeah, giving types a runtime footprint is on the official list of non-goals for the TypeScript project: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Desi...

      • noduerme 4 years ago

        Interesting.

        > Instead, encourage programming patterns that do not require run-time metadata.

        I can understand why. But there are times when any alternative pattern is a worse choice. Then it's left to the coder to hack together type checks however it's convenient. Does the Dog class really need a static variable typed to Dog|Cat... ? It would be nice if instead of a dozen nonstandard ways to do that, some metadata could be emitted if you wanted to. TS does weird stuff like emitting functions to define enums, for instance.

        • brundolf 4 years ago

          > Does the Dog class really need a static variable typed to Dog|Cat... ?

          It doesn't- because classes are a native JS concept and not a TS concept; they already have the metadata to distinguish them

          > TS does weird stuff like emitting functions to define enums, for instance

          Not sure what's meant by this; TypeScript does introduce enum syntax which technically generates runtime code (one of the few instances), though it's just very light sugar over constants, and using it is also sorta advised against these days

          • noduerme 4 years ago

            > classes are a native JS concept

            Recently, with ES6. But JS still lacks a way to get the type of an instance. You can check it but you can't simply get it, as in `new typeof(this)`. So you have to store it somewhere in the instance or in the type, as you would for a manual typeguard. And much of TS usage is still not for generating ES6 code. TS overlays boilerplate to mimick classes on ES5 targets. As it mimicks enums. I'm just saying I don't see why it couldn't mimick type reflection as well.

theK 4 years ago

Am I the only one that feels uncomfortable with all that usage of strings in TS’s type system? Why not use pure literals instead of string literals? This is a genuine question, I’m trying to find out what the pros and cons where in the decision making process.

  • p1necone 4 years ago

    In the context of type definitions, strings aren't really regular bare strings.

    I.e. if I write

      type foo = {
         bar: "Vodka"
      }
    
    I'm guaranteeing that the value of 'bar' on any object of that type must be "Vodka" - the type of bar is not string, it's literally "Vodka" because string values can be types.

    This seems a little obscure and pointless, but you can put these string types in a union, and enforce that a value must be one of many values.

      function doTheThing(color: "RED" | "GREEN") {
          //do stuff
      }
    
      doTheThing("RED"); //ok
      doTheThing("GREEN"); //ok
      doTheThing("ORANGE"); //doesn't compile, because the type of the param *isn't* string, it's "RED" | "GREEN"
    
    You can also define a type like

      type Mountain = { name: "EVEREST", height: 8848 } | { name: "K2", height: 8611 };
    
    and then at compile time know that if mountain.name === "EVEREST" then height === 8848 because the types of name and height aren't string and number, they're "EVEREST" | "K2" and 8848 | 8611, and the compiler is smart enough to work out one based on the other.

    ==============================================

    For extra context - a lot of this is for interoptability with Javascript code - you want to call some Javascript function with a stringly typed enum, and enforce only passing in valid values, but the Javascript code still just deals with strings.

    A lot of Typescripts kind of insane flexibility is so you can introduce type safety to all sorts of dynamic Javascript code, without having to make sacrifices on the dynamic-ness of it.

    Turns out this flexibility is actually kinda awesome to have in general even when you're not trying to refactor an existing JS codebase.

    • bobbylarrybobby 4 years ago

      And don't forget about [template literal types](https://devblogs.microsoft.com/typescript/announcing-typescr...)!

      ```

      type Color = "red" | "blue"; type Quantity = "one" | "two";

      type SeussFish = `${Quantity | Color} fish`; // same as // type SeussFish = "one fish" | "two fish" // | "red fish" | "blue fish";

      ```

    • RussianCow 4 years ago

      To add to this, the reason it’s important for TypeScript to support string literals as values instead of using enums or something new is interop with existing JS. For example, think of an event handler where you pass the event name as the first argument; you can ensure at compile-time that an invalid event name isn’t passed.

      • chrismorgan 4 years ago

        Or more importantly, varying the event type based on the event name.

        e.g. addEventListener("click", (MouseEvent): void) versus addEventListener("input", (InputEvent): void)

    • Sirenos 4 years ago

      That's really interesting.

          doTheThing(readFromUser())
      
      What does the compiler do in cases where you have strings coming in like this?
      • paavohtl 4 years ago

        It gives a type error, because strings are not assignable to string literals.

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

        However, if you check or assert that the returned value is one of the accepted literals, compiler accepts it:

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

        This is one of the classic examples of flow-sensitive typing in TS.

      • phpnode 4 years ago

        it'll tell you that `string` cannot be assigned to type `"RED" | "GREEN"`, and so you'll need to write a function that refines the type correctly, e.g.

            function isValidInput(input: unknown): input is "RED" | "GREEN" {
              return input === "RED" || input === "GREEN";
            }
        
            const input = readFromUser();
            if (isValidInput(input)) {
              doTheThing(input); // no type error
            }
      • moron4hire 4 years ago

        It's an error, as the plain `string` type doesn't satisfy the union. You'll need to perform type assertions to make the return type down before passing it as a parameter.

    • Joker_vD 4 years ago

      The academic term for such types is "singleton types". They allow for some quite mind-blowing possibilities as you've shown.

  • girvo 4 years ago

    As others have pointed out, they're not really "strings" per se, or at least can be a lot stricter than a string might seem when used right. We use that constantly for useful things.

    The bigger reason why it is this way is to ensure you can write type annotations for stringly-typed Javascript. While Typescript is it's own thing really at this point, it still is very focused on making it possible to type-annotate JS code.

        type Event = 'onClick' | 'onHover'
    
        // some stringly typed javascript can now be nicely typed
        something.triggerEvent('onClick', someData)
    
    It lets you do some really nice things, and is quite strongly typed despite how it looks at first glace, while still letting me write some strongly-typed definitions for existing Javascript code that was very much __not__ written with types in mind.
    • ygra 4 years ago

      In a similar case TypeScript even knows the signature of the event handler for addEventListener, and what type of Element document.createElement('div') returns. Someone even built typings for querySelector that parse the selector and returns the correct element type.

      In general, however, I still think APIs written in TypeScript first are a lot cleaner type-wise than typings retrofitted onto a JS API. And having TypeScript as an afterthought these days doesn't seem like a good path to go anymore.

      • girvo 4 years ago

        > In general, however, I still think APIs written in TypeScript first are a lot cleaner type-wise than typings retrofitted onto a JS API

        Oh absolutely. Writing JS-first just seems like a lost cause. But there are still enough internal libraries and such written in very JS JS floating around that I come across at work that we need to interface with that I appreciate how much effort the TS team still puts into making sure I can drag those libs kicking and screaming into 2022.

    • sanitycheck 4 years ago

      I found it interesting that I would never use strings this way in JS, but TS seems to encourage it. I definitely would not want to strip typing out of my TS code and try to maintain it as plain JS.

      • xsmasher 4 years ago

        What would you use for a field called "stage" that could be set to 'dev' or 'stage' or 'prod'? Or other enum-type things?

        I've seen pure JS enums like {'dev' : Symbol('dev'), 'prod' : etc} but I never use them because you can't send them over the wire.

        You could use a type like {dev:true} | {prod:true} | {stage:true} but you still wind up comparing strings.

        • mmis1000 4 years ago

          > {dev:true} | {prod:true} | {stage:true}

          This can't be accessed safely. Because all value in typescript can be sub type of how it typed.

          It means code following will pass.

              var a = { dev: true, get prod() { throw Error() } }
              var b: { dev:true } | {prod:true} | {stage:true} = a
          
          
          And there is no guarantee that access any of these fields is safe.
          • xsmasher 4 years ago

            That did not compile for me with my TS settings;

            > Type '{ dev: boolean; readonly prod: void; }' is not assignable to type '{ prod: true; }'

            It's still a bad solution though; it allows nonsense values like {dev:true, prod:true}.

            • mmis1000 4 years ago

              That is because I forgot to assert the true as literal true, so it defaults to be boolean (where both true and false are allowed)

  • staticassertion 4 years ago

    I think it's kind of cool. It reminds me a bit of C++ templates, which are "compile time duck typed". The benefit is that you get lose typing constructs that are evaluated strictly and at type check time, which is kinda nutty. It means that you can lean really heavily on the compiler.

    Sort of like using Python to generate Typescript, as random example - my Python code doesn't have to typecheck, but its output does, so I can do absurd shit in Python and still feel good about the output.

    One example is something like this: https://www.typescriptlang.org/docs/handbook/2/template-lite...

    These are types that are built from string templates. Since strings are loose and can be manipulated in crazy ways like appending, we can now manipulate types in the same way. We can write types that are themselves parsers.

    So idk if that's good or not, the downside in C++ is that TMP errors are fucking insane, but the upside is that I can have a function that says "pass me something and I'll call "iter" on it and I don't care what that thing is".

    It also feelsy kinda more "typey". Types are just values. Types are just strings or numbers or whatever. They're things, with the constraint being that they must exist concretely (or be inferrable) when the type checker runs. No distinction between types and values seems like it's the ideal.

    • spiralx 4 years ago

      Which allows for things like this type that implements a simplified SQL query parser checked against a provided 'database' object:

      https://github.com/codemix/ts-sql

      This project was my go-to "nifty but pointless" example for TS string literal types before this article :)

uninformed 4 years ago

Yo dawg, I heard you like Typescript ... you know the rest.

throwaway17_17 4 years ago

This article is intriguing, I liked the playful tone, particularly in light of the subject. I’m hoping the talk gets streamed to YouTube.

This also makes me interested in the formalization attempts I have heard are ongoing for TS’s type system. Anyone with good paper/videos on that development feel free to drop a link.

  • rrishi 4 years ago

    Which article were you referring to?

    The link in the post goes to a GitHub repo.

adamddev1 4 years ago

This guy's like, "I see your Lisp in Lisp and raise you..."

finiteparadox 4 years ago

Incredible, one would expect this to be theoretically possible since Typescript's type system is Turing complete, but it is certainly different to see it done in practice. Wow!

orthoxerox 4 years ago

Can they do Tetris next?

https://github.com/mattbierner/Super-Template-Tetris

  • phpnode 4 years ago

    Tetris is challenging because TS escapes newlines in type output, so you'd end up with a bunch of\nwhich makes it\nhard to read the output. It could probably be done using arrays instead, but that gets truncated so you'd have to have a very small "screen"

culi 4 years ago

For people used to working in TypeScript who suddenly find themselves having to work on a vanilla js app... and don't wanna use JSDoc?

  • sakarisson 4 years ago

    I'd convince the team to switch to typed or get out of the project

  • difu_disciple 4 years ago

    Do everything you can to introduce TS incrementally. JSDoc simply isn’t enough.

  • jschrf 4 years ago

    Use TS locally

    • LASR 4 years ago

      You can secretly have super powers.

    • LAC-Tech 4 years ago

      What does this mean?

      • schwartzworld 4 years ago

        I think OP means that if your project / job doesn't support typescript, you can write .ts files on your local machine and then compile them to .js. I think it would be very challenging though to be the only person on a project using TS though.

        • LAC-Tech 4 years ago

          Yeah that would be weird.

          Using tsc to type check javascript - with JSDoc type annotations - works fine. You're really not missing much from "real" typescript, and you can save yourself a transpile.

          • epolanski 4 years ago

            It doesn't really work fine beyond a single file.

            I tried the experiment of .js files driven by typescript in JSDocs for around 10k lines and there is a night and day difference in what you can express, how and reuse it at the type level.

            • LAC-Tech 4 years ago

              I'm experienced in doing this too.

              Name one thing you can't do.

              To clairfy, if I am sharing types between modules I tend to use .d.ts files. Still no transpiling needed.

          • no_wizard 4 years ago

            until you need to type the shape of objects, and want to share that shape with other files. You can't import JSDoc types from other files. At which point, you would be forced to create `types.d.ts` file or similar. Might as well be writing typescript.

            The limitations are too much for complex projects at scale. I strongly believe this is why Closure types didn't take off.

            That and Google's lack of developer relations around the project

            • LAC-Tech 4 years ago

              You can use types written in a .d.ts file in javascript. You reference them with the JSDoc annotations.

              And no it's not the case you may as well write typescript at that point - because you're still using the actual language the browser runs, and not transpiling.

            • mikewhy 4 years ago

              > You can't import JSDoc types from other files

              Yes, you can. Using `import('./someFile').SomeType`

              Though I do agree, JSDoc works, but isn't as nice as directly writing types in a .ts file

revskill 4 years ago

Can we read the source file from filesystem, then pass into type checker like this ?

  • chii 4 years ago

    that sounds increasingly like a build-time macro system...

evolveyourmind 4 years ago

This is mindblowing

andrewstuart 4 years ago

I love TypeScript but its complexity is getting ridiculous.

wikitopian 4 years ago

Oh thank God. There aren't nearly enough ways to write javascript.

Keyboard Shortcuts

j
Next item
k
Previous item
o / Enter
Open selected item
?
Show this help
Esc
Close modal / clear selection