Skip to content

Support typescript with --experimental-strip-types  #208

Closed

Description

@marco-ippolito

Add this issue to keep track of the work for supporting typescript out of the box, in the PR you can find documentation etc...
The main idea is to use type-stripping.
This is achieved ideally through the use of an external dependency, in my opinion @swc/wasm-typescript.
I would like to have something basic and usable to propose to the public.

Tracking points to discuss:

  • Support commonjs syntax
  • SourceMaps (enabling them with a flag)
  • Type stripping on .cts
  • Add a flag to enable transformation to support TS only features (enums etc...)
  • Extension guessing (.js because of transpilation => but at runtime is a TS)
  • Transpiling .ts files in the node_modules
  • TypeScript linting
  • Externalize swc wasm

Roadmap: #217

Activity

  1. mcollina commented on Jul 2, 2024

    @mcollina
    SponsorMember

    I have two concerns (none of them blocking):

    1. Is swc on board with following our LTS? This seems a massive dependency we should not take on lightly.
    2. TypeScript does not follow semver, and the language evolves relatively quickly. Is this approach stable long term? Everybody seems always to be supporting the latest version of TypeScript.

    My proposal in nodejs/node#43818 was to have a strategy for doing this automatically but not vendor anything. Something along the lines of:

    $ node script.ts
    Typescript support is missing, install it with:
    npm i --location=global typescript-node-core
    $ npm i --location=global typescript-node-core
    ...
    $ node script.ts
    "Hello World"

    An alternative (possibly better for long-term support):

    $ node script.ts
    Typescript support is missing, install it with:
    npx ts-node-core
    
    Learn about TypeScript support at: ...
    $ npx ts-node-core
    Detected Node.js v24.42.0
    What typescript support would you like?
    [ ] type stripping - swc@22
    [ ] type checking - tsc@23
    
    ...Installing
    
    $ node script.ts
    "Hello World"
  2. marco-ippolito commented on Jul 2, 2024

    @marco-ippolito
    MemberAuthor

    To answer to your points:

    1. SWC seems to be a mature project, it is already used by Deno for our same purpose, has Apache-2 license which is fine.
      I'm using @swc/wasm, the wasm version to avoid compiling rust. We probably should ask the blessing from the maintainer @kdy1 and maybe release a version that we dont have to patch 😄
    2. With typestripping we dont really care what version of typescript the user is using because we just remove it (unless there is some syntax not supported by SWC), we dont perform type checking at all. I think SWC moves quickly enough.
      Also asking the user to install a dependency is imho a suboptimal DX, users just want to be able to run node foo.ts

    If this proposal is accepted we could also add hooks to customizate the transpilation, and many more things that come out of the box from swc

  3. kdy1 commented on Jul 2, 2024

    @kdy1
    Member

    I can configure a publish pipeline for a separate package/binary, or a Wasm publish pipeline if node.js will use SWC for this task.

  4. marco-ippolito commented on Jul 2, 2024

    @marco-ippolito
    MemberAuthor

    I can configure a publish pipeline for a separate package/binary, or a Wasm publish pipeline if node.js will use SWC for this task.

    If we are able to ship the initial PR with v1.6.6 for the next updates that would be amazing, thank you

  5. benjamingr commented on Jul 2, 2024

    @benjamingr
    Member

    +1 for all the suggestions and also +1 for @kdy1 have nothing but nice things to say about him and swc + there is prior art in other runtimes.

  6. GeoffreyBooth commented on Jul 2, 2024

    @GeoffreyBooth
    Member

    If what we ship is type stripping, then we’re essentially shipping experimental support for the Type Annotations proposal. Which is good! I feel like that’s what does make sense to include in Node core, and I hope that proposal graduates and V8 adds support and then we can drop our support for doing it ourselves.

    In that vein, maybe a flag of --experimental-strip-types might make more sense? To clarify to users that our intended scope is not to support all of TypeScript’s syntax or features, such as paths or enums or type checking; we just strip types and that’s it, and if they want more they can register module customization hooks from a userland library. And type stripping should be stable and unlikely to change as TypeScript evolves.

  7. marco-ippolito commented on Jul 2, 2024

    @marco-ippolito
    MemberAuthor

    If what we ship is type stripping, then we’re essentially shipping experimental support for the Type Annotations proposal. Which is good! I feel like that’s what does make sense to include in Node core, and I hope that proposal graduates and V8 adds support and then we can drop our support for doing it ourselves.

    In that vein, maybe a flag of --experimental-strip-types might make more sense? To clarify to users that our intended scope is not to support all of TypeScript’s syntax or features, such as paths or enums or type checking; we just strip types and that’s it, and if they want more they can register module customization hooks from a userland library. And type stripping should be stable and unlikely to change as TypeScript evolves.

    Not against renaming the flag, but for this very limited features that we are adding, --experimental-typescript makes more sense because we are actually only supporting .ts files, (no tsx or other flavors), but once this lands and remove the flag, we will think on how to make it customizable, and increase the number of flavor, that would be a real type-stripping

  8. GeoffreyBooth commented on Jul 2, 2024

    @GeoffreyBooth
    Member

    This was discussed today in the loaders meeting:

    • I think there was general agreement that type stripping (but probably not more) is something that we would like to see in core, either soon or eventually.
    • @joyeecheung asked that @marco-ippolito’s branch be implemented as a package similar to Undici that can be developed outside of the core release cycle and eventually merged in when it’s mature.
    • To exist in core, support would need to both take advantage of being in core, such as being native; and avoid implementing features of TypeScript that may change faster than our release cycle (such as type checking, and any transforms such as enums, decorators, ESM-CJS transpilation and others).
    • @legendecas will schedule a meeting with @marco-ippolito and some champions of the Type Annotations proposal to discuss collaboration and next steps toward potentially implementing that proposal in Node core.
  9. added
    loaders-agendaIssues and PRs to discuss during the meetings of the Loaders team
    and removed
    loaders-agendaIssues and PRs to discuss during the meetings of the Loaders team
    on Jul 2, 2024
  10. kdy1 commented on Jul 3, 2024

    @kdy1
    Member

    I created https://www.npmjs.com/package/@swc/wasm-typescript, which barely strips out the type.

    And I wrote documentation for it at https://swc.rs/docs/references/wasm-typescript

  11. GeoffreyBooth commented on Jul 3, 2024

    @GeoffreyBooth
    Member

    which barely strips out the type.

    How can this be used to just strip types and nothing more?

  12. kdy1 commented on Jul 3, 2024

    @kdy1
    Member
  13. GeoffreyBooth commented on Jul 3, 2024

    @GeoffreyBooth
    Member

    See swc.rs/docs/references/wasm-typescript

    I saw that. What settings would I use to just strip types and nothing more? No support for enums, decorators, transpiling module import/export, etc. As in, if I wanted to use this to implement the Type Annotations proposal and error on any syntax that requires transforms.

  14. kdy1 commented on Jul 3, 2024

    @kdy1
    Member

    It's not transform() of @swc/core. It's a completely different thing.

    It

    • strips type
    • supports enum
    • does not support transpiling decorator (parsing/codegen is possible with a config)
    • import/exports are not touched, except type-only imports. (Including imports not used as values, per config)

    These operations is one typescript set, but I can remove the enum part and/or handling code for type-only imports.
    I'll add options for them soon.

  15. 65 remaining items

  16. GeoffreyBooth commented on Jul 14, 2024

    @GeoffreyBooth
    Member

    catering to such more general users seems to be the original goal of this effort.

    I was the one who defined the original goal of this effort 😄 At least when we first starting discussing type-stripping a few weeks ago. The goal was to find something that we could ship within Node that would survive a three-year LTS period, despite TypeScript itself not following semver. At the time we assumed that TypeScript transforms would need to be ruled out because we assumed that they weren’t stable enough for a three-year lifespan, and fortunately the TypeScript team has informed us that that isn’t the case, so it’s an option to support them; but still, supporting them incurs a performance penalty. Perhaps less of a performance penalty if they can be transformed with the goal of trying to preserve line and column numbers, saving us from needing to generate a source map, but that’s a challenge for SWC and out of scope for Node.

    Would you say the existence of something like this will make the TSC more willing to consider full TypeScript syntax support?

    I can only speak for myself, not the TSC. I think you need to look at what some of the TypeScript team have been saying, though: “full” TypeScript support isn’t just type-stripping plus transforms, it’s also type checking and supporting all the options within tsconfig.json. Node can’t ever include all of that in core, because the type checking and tsconfig options very much do change within a 3-year cycle. So even if we support type stripping plus transforms, we’re still going to leave many users unhappy because of the other things that get left out.

    And the solution for those users is to just use TypeScript in full, via tsc or --import=tsx or Vite or something similar. Those solutions already exist and will continue to exist, and Node will continue to support and encourage them; our error messages and docs will point people in their direction to get whatever features they want that Node can’t provide. Fundamentally it’s just not possible to satisfy both of the user requests of “full” TypeScript and no breaking changes for a three year LTS cycle. Either the TypeScript will get very out of date or it won’t be stable, and I consider both of those options to be worse than the two-option menu of type-stripping plus hooks for external tools.

    I can imagine you asking “so if bundling the full TypeScript experience is off the table, what about the semi-full TypeScript of just type-stripping plus transforms?” And my reply is to just look at the work I did to migrate Corepack. Maybe 5% of it was dealing with the public class property and the enum, and I could’ve migrated the enum in a much simpler way than I did. Much more time was spent on updating import specifiers, though I’m sure that could be automated (and migrating most if not all syntaxes like enums could probably be automated too). In other words, transforms aren’t much of an issue, and they have modern equivalents that are strippable, so I think it’s a better tradeoff to ban them and get faster performance than support something that could be deprecated. It’s a certainty that any built-in TypeScript support that Node has won’t be able to run all existing TypeScript code, so if people are going to need to migrate at all (whether by hand or via a tool) they might as well migrate to the most performant syntaxes rather than Node sacrificing some performance to save some of the migration work. And finally, our target audience for this feature is users starting new projects, not users migrating existing ones, as the latter users by definition already have a solution that works for them. New users starting from scratch can work within the constraints of type stripping if that’s how they want to run their TypeScript, and presumably an ecosystem will grow with linting tools and the like for helping guide them. And for everyone else, as I keep saying, tsc and tsx and Vite and tsimp and ts-node and so on will continue to exist and continue to work, and continue providing fuller experiences than Node ever can.

  17. yyx990803 commented on Jul 14, 2024

    @yyx990803

    First of all, by "full syntax support" I mean a transform-only support, like what esbuild / swc / oxc does with isolatedModules: true. Never intended to want type-checking, tsconfig options etc.

    Perhaps less of a performance penalty if they can be transformed with the goal of trying to preserve line and column numbers, saving us from needing to generate a source map, but that’s a challenge for SWC and out of scope for Node.

    It's not out of scope. If such transform exists with negligible perf overhead compared to strip-types-only, then the argument for Node.js to go with strip-types-only becomes much weaker. In fact, I am raising this question because I am very confident this is something feasible, therefore the assumption that "strip-types-only has big perf wins" needs to be challenged.

    Out of curiosity I ran the same benchmark on your modified corepack source using oxc-node:

    Benchmark 1: node --import @oxc-node/core/register ./corepack/sources/_cli.ts
      Time (mean ± σ):      93.4 ms ±   1.4 ms    [User: 99.8 ms, System: 13.4 ms]
      Range (min … max):    91.4 ms …  96.7 ms    30 runs
    
    Benchmark 2: node --import tsx ./corepack/sources/_cli.ts
      Time (mean ± σ):     136.0 ms ±   0.8 ms    [User: 161.1 ms, System: 21.4 ms]
      Range (min … max):   134.0 ms … 137.5 ms    30 runs
    
    Summary
      node --import @oxc-node/core/register ./corepack/sources/_cli.ts ran
        1.46 ± 0.02 times faster than node --import tsx ./corepack/sources/_cli.ts
    

    The perf is 1.46x over tsx vs the 1.22x of current --experimental-strip-types. Granted this is native binary vs wasm, but this one is actually running a full transform with source maps, and even respecting resolution config in tsconfig.json. Even both as native binary, an oxc-based position-preserving + no AST allocation TS transform can very possibly out perform other strip-type-only implementations. SWC can also implement the proposed transform on top of the current strip-type-only implementation and compare the result.

    In other words, transforms aren’t much of an issue

    I fundamentally disagree with this. Removing these small discrepancies avoids a Node-flavored subset of TS. Right now these discrepancies are justified in the name of performance - but as I said, the assumption that "strip-types-only has big perf wins" needs to be challenged.

    and presumably an ecosystem will grow with linting tools and the like for helping guide them.

    This is encouraging yet another flavor of TS that only works in Node, creating more fragmentation / confusion in an already complicated landscape, but the other side of the tradeoff is questionable for the reasons outlined above.

    "Node.js supports full syntax-level TS transforms, but not type checking / tsconfig" is a much cleaner definition than "Node.js supports a subset of TS syntax". It aligns with all the other transforms users are already using via esbuild / swc / oxc etc.

    My whole argument is that it is not a good idea to intentionally steer away from possible ecosystem alignment in the name of performance, especially when the performance gain might not be as significant as imagined.

  18. jakebailey commented on Jul 14, 2024

    @jakebailey
    Member

    I think you need to look at what some of the TypeScript team have been saying, though: “full” TypeScript support isn’t just type-stripping plus transforms, it’s also type checking and supporting all the options within tsconfig.json

    I don't think anyone has actually advocated for checking; that is of course the bit that "doesn't follow semver", though the general scheme is that "any change could be a breaking change". The config defaults are what change in semver major, and the API is otherwise very stable. Not that using TS itself here is a goal; any third party emitter can handle TS code if it complies with isolatedModules.

    I mean a transform-only support, like what esbuild / swc / oxc does with isolatedModules: true. Never intended to want type-checking, tsconfig options etc.

    Note that there are a couple of options respected by those tools which affect emit, namely useDefineForClassFields (and target after a specific version) and experimentalDectorators. These aren't affected by isolatedModules, and I'm not sure what swc does at this point (I think it leaves define class field semantics on by default, differing to TSC?)

  19. GeoffreyBooth commented on Jul 14, 2024

    @GeoffreyBooth
    Member

    My whole argument is that it is not a good idea to intentionally steer away from possible ecosystem alignment in the name of performance, especially when the performance gain might not be as significant as imagined.

    Sure. Right now, though, the transpiler you describe that can do transforms yet still not need source maps doesn’t exist, as far as I know. If someone builds that, and we measure the performance difference and it’s truly negligible, then it’s something to consider. We can always add support for transforms later, even after this goes stable.

    But my point about looking at the changeset for the Corepack migration was that transforms account for very little of it, and the rest will still remain even if we support transforms: adding file extensions, adding with { type: 'json' }, using import type, etc. There are also some potential migrations that Corepack didn’t need but that many apps would, of avoiding tsconfig paths and adjusting to ESM-syntax code running as ESM rather than being transpiled into CommonJS (so no global require intermixed with import/export, etc.). All of these Node-specific idiosyncracies would remain even if we start supporting transforms. Maybe some of them we might find ways to get rid of, but I doubt we’d eliminate all of them.

    And so then yes, Node’s built-in support will support a particular subset of TypeScript’s full feature set, and users wanting to use that support instead of tsc or tsx or Vite etc will need to live within those limitations. Probably everything that would need to change could be automated, from adding file extensions to splitting import/import type to migrating from tsconfig paths to package.json "imports" and so on. And if that’s the case, I’m not sure why transforms are such a concern as compared with any of the other things that ecosystem tools might support that Node might not.

  20. yyx990803 commented on Jul 14, 2024

    @yyx990803

    If someone builds that, and we measure the performance difference and it’s truly negligible, then it’s something to consider. We can always add support for transforms later, even after this goes stable.

    Great. That's pretty much what I want to hear.


    Surely there will be some migration costs that cannot be covered by transforms, but don't we agree that the smaller the differences are, the better?

    More importantly we need to evaluate these differences in context - things like tsconfig paths are much easier to explain because Node.js' own module resolutions rules need to be the source of truth, but explaining that

    class Foo {
      construct(public id: number) {}
    }

    ...works as expected everywhere else but just not in Node.js (which claims to "support TypeScript") seems... very awkward.

  21. marco-ippolito commented on Jul 14, 2024

    @marco-ippolito
    MemberAuthor

    @kdy1 do you think is possible to perform transformation of ts features and preserve the position in the file with whitespacing?

  22. arcanis commented on Jul 14, 2024

    @arcanis
    Contributor

    @arcanis I wanted to see how hard it would be to migrate an existing app to use --experimental-strip-types, so I migrated Corepack: https://github.com/nodejs/corepack/compare/main…GeoffreyBooth:corepack:strip-types

    I didn't ask whether the migration was hard to someone well versed in Node.js. I did ask however:

    • Who is this feature built for? What is the intended audience?
    • Why is this feature designed by default as a departure from status quo?
    • Why have other tools been able to support transforms, and Node.js would be unable to?
    • Why is Node.js taking ownership of the syntax allowed in a .ts file?
    • What are examples of packages shipped to run as TS, and do they use enums?

    As for whether the migration would be easy or hard, that matters much less to me than having a migration. As designed, the feature would force people out of specific TS syntax constructs, which seems an overreach to me.

  23. kdy1 commented on Jul 14, 2024

    @kdy1
    Member

    @marco-ippolito It's partially possible, but it's a bit hard in general.

    Basic restrictions are

    • all transformed results have exactly the same order as the input syntax.
    • it should be at most lexing + parsing + single visit (because of the performance)

    We need to evaluate each syntax under this assumption.

    Enums

    Enums are typically written as

    enum Foo {
        a = 1,
        b = 2, c = 3
    }

    and it can be transpiled as

    (function(Foo) {
        Foo[Foo["a"] = 1] = "a";
        Foo[Foo["b"] = 2] = "b"; Foo[Foo["c"] = 3] = "c"; 
    })(Foo || (Foo = {}));

    so it can be supported.

    Decorators

    The decorator pass is too complex to be supported with this kind of transpilation. There's no way to preserve token order.

    Constructor properties

    I'm not sure if this is possible or not, but my guess is that it's impossible in some cases, meaning trying to support it may result in forcing specific formatting for the users.

    If you have some ideas, please share

  24. yyx990803 commented on Jul 14, 2024

    @yyx990803

    @kdy1 I shared an example here: #208 (comment)

    Since decorators are on standards track and already stage 3, I don’t think they need to be covered by the TS transform in the long run. Short term it may be the only exception.

  25. marco-ippolito commented on Jul 14, 2024

    @marco-ippolito
    MemberAuthor

    I'm willing to support full ts features, behind a flag, if we see it works well and it's stable, we will as well remove the flag and set it to default.
    Example:

    node --expertimental-strip-types --enable-ts-transform  foo.ts

    And with source maps support

    node --expertimental-strip-types --enable-ts-transform  --enable-source-maps foo.ts

    The current swc version does not support this so it's gonna be in a next iteration.
    If we can make it work without source-maps and with whitespacing it would be even better

  26. GeoffreyBooth commented on Jul 14, 2024

    @GeoffreyBooth
    Member

    Who is this feature built for? What is the intended audience?

    • Users writing scripts without package.json or dependencies, where the user wants to use TypeScript syntax.
    • Users starting new projects where they can write using type stripping-compatible TypeScript syntax/settings from the start, who prefer the speed of built-in native compilation. (We're using Wasm SWC currently, but the intention is to replace it with a native build once we can figure out how to get Rust into our build chain.)

    Why is this feature designed by default as a departure from status quo?

    • Node already supports the “status quo”, via --import=tsx.
    • One of the primary reasons that anything gets included in core as opposed to being left to userland is because the feature can be faster when included in core, usually by being implemented using native code. Especially since TypeScript via hooks is already possible, and that supports the TypeScript that users are already familiar with, this built-in support should be optimized for performance.
    • The proposed behavior matches the Type Annotations proposal, which might become a future status quo (and which many of us support). If that occurs, then type stripping will just become standard JavaScript, and code written for type stripping today will work in any runtime including browsers; and Node will be able to simplify our internals by removing SWC, since V8 would handle the type stripping for us.

    Why have other tools been able to support transforms, and Node.js would be unable to?

    • Node already supports transforms, via --import=tsx.
    • Node could support transforms via its built-in support as well, either enabled all the time or via a separate opt-in flag, but I think we would want to do so only if performance were not noticeably impacted. A main advantage of the built-in support is theoretically improved performance, so we wouldn’t want to noticeably degrade that to support features that users can already access via --import=tsx. Or put another way: if type stripping lands, Node will have two methods for supporting TypeScript. It’s better if one of them is fast, even if slightly limited, rather than both of them being slow and equivalent.
    • Transforms are not part of the Type Annotations proposal, so supporting them muddies the transition to that potential future where that proposal graduates. Since transforms are already supported via --import=tsx, it might be preferable to keep type-stripping aligned with that proposal as it advances, at least until it either graduates or is abandoned.

    Why is Node.js taking ownership of the syntax allowed in a .ts file?

    • Because no one else is? Has there been a proposal from the TypeScript team for adding built-in support of TypeScript in Node?
    • Type stripping is primarily inspired by the Type Annotations proposal, of which TypeScript’s @DanielRosenwasser is a champion. This is arguably just as blessed a syntax or standard as the moving target of whatever a given version of tsc supports.

    What are examples of packages shipped to run as TS, and do they use enums?

    I don’t know of any. As the TypeScript team has explained, shipping packages written in TypeScript is generally a bad idea, unless you’re in control of your consumers (for example, if it’s meant for use only by you in your own app, or for the members of your team). Even if there are packages out there that don’t use enums, they probably don’t follow all of the rules of --experimental-type-stripping (see the things I changed in Corepack). The goal isn’t to support existing published packages written in TypeScript, if there are any.

  27. jakebailey commented on Jul 15, 2024

    @jakebailey
    Member

    The proposed behavior matches the Type Annotations proposal, which might become a future status quo (and which many of us support). If that occurs, then type stripping will just become standard JavaScript, and code written for type stripping today will work in any runtime including browsers; and Node will be able to simplify our internals by removing SWC, since V8 would handle the type stripping for us.

    Type stripping is primarily inspired by the Type Annotations proposal, of which TypeScript’s @DanielRosenwasser is a champion. This is arguably just as blessed a syntax or standard as the moving target of whatever a given version of tsc supports.

    I would still shy away from this as a motivator; the Type Annotations proposal may not cover all TS syntax. Not only that, but it can't, because in TypeScript the expression foo<number>(value) is a function call with type arguments, but in JavaScript that's two comparisons. This means that the type annotations proposal will never contain all valid TypeScript syntax today. As such --experimental-strip-types would a different subset of the language (so not "matching" the TC39 proposal), and can't be eventually replaced by something from V8.

  28. marco-ippolito commented on Jul 25, 2024

    @marco-ippolito
    MemberAuthor

    Since this has landed and we have a roadmap I'll close this issue, we can talk about next steps here #217

  29. sdegutis commented on Aug 10, 2024

    @sdegutis

    @marco-ippolito I want to help as much as I can to get this feature working thoroughly before 2025. I have nothing but free time. Please let me know how I can help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    typescriptDiscussions related to TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions