Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Isolated Declarations in TS 5.5: State of the feature #58944

Open
1 task done
robpalme opened this issue Jun 20, 2024 · 20 comments
Open
1 task done

Isolated Declarations in TS 5.5: State of the feature #58944

robpalme opened this issue Jun 20, 2024 · 20 comments
Labels
Discussion Issues which may not have code impact

Comments

@robpalme
Copy link

Acknowledgement

  • I acknowledge that issues using this template may be closed without further explanation at the maintainer's discretion.

Comment

Isolated Declarations in TS 5.5: State of the feature

This post is intended to be a status update on Isolated Declarations as of mid-June 2024. There are separate messages for different audiences who may be interested in this feature.

Audience: Users

For folk who wish to enable the --isolatedDeclarations (ID) flag in their project, please go ahead! Just be aware of what you are opting into and what you will get back in return.

ID enforces stricter constraints above and beyond regular TS. It's a bit like --isolatedModules in that way. But that's it. Your TypeScript project will probably show new errors, and you are encouraged to use the new Quick Fixes to hopefully fix all of those errors. We believe the flag and errors are stable. The code fixes are mostly stable though we know of some issues with non-optimal generated annotations.

Please do not expect anything amazing to happen when you enable the flag. Your declaration emit won't change on enablement. Likewise if you later remove the flag, the declaration emit will remain the same. Compilation won't magically go faster. This is because currently there is no TypeScript infrastructure that takes advantage of the possibilities ID offers other than the one exception, which is the ability to use the transpileDeclaration() API.

We expect this situation to change over time as the feature matures and ecosystem tooling is built on top. Think of ID as being a Minimum Viable Product (MVP) for now. That won't last long - we know of concrete interest from tooling to leverage ID.

Potential reasons why you would want to enable this option today anyway:

  • Courage: You are excited to try new stuff and want to provide feedback 🙏
  • Style: You like the idea of explicitly annotated exports 💅
  • API Access: You want to use the new transpileDeclaration()
  • External Tooling: You are pairing this feature with some other tool or system that DOES offer some kind of advantage 😎

There's also some work in progress to improve ergonomics that we hope to land in TS 5.6. For example, some way of handling computed properties. Lowering the barrier to adopt ID is something we'll keep looking at.

Audience: Declaration Emitter Tool Authors

For folk who think their time has come to build the fastest-ever declaration emitter, or other creative goals, you are of course free to try.

However, please be aware of an important caveat: You may struggle to precisely replicate TypeScript's declaration emit. It will be possible to exactly match it in many cases, but in others, you will find that TypeScript is using its advanced checker features to synthesize the output in a way that is hard to replicate. And therefore unless you also implement the checker's behavior - which could be a tall order - the output will differ.

Of course, you are always welcome to invent your own declaration emit for these cases. Just be aware that these may not be fully compatible with what TypeScript itself produces today or in the future.

Our plan is to continue the work on a low-complexity Unified Emit and hopefully complete it in TypeScript 5.6. The goal is to ensure that all of TypeScript's declaration emit under ID is trivially replicable without the need for a type-checker. We plan to add test infrastructure to ensure that future evolution of TypeScript's declaration emit does not rely on "forbidden knowledge" from the checker or from anything other than the single file being transformed. The intent is to ensure that as features are added to TypeScript in future, ID emit will always remain trivially replicable by third-party single-file declaration emitters.

We're highly interested in getting feedback from implementers of these tools. Please feel free to get in touch here.

Audience: Build Orchestrator Tool Authors

One of the promises of Isolated Declarations was to permit higher-level build tooling to perform declaration emit and type-checking in parallel across monorepos to deliver performance wins. If you want to build such a tool, please go ahead now!

As of TypeScript 5.5 this type of tooling can be built on top of the new transpileDeclaration API. You do not need to wait for third-party Declaration Emitter Tools to be built.

In the 5.6 nightly and beyond, the internal flag that transpileDeclaration relies on (noCheck) is publicly exposed. This allows parallelism of various emit and checking without necessarily going as far as ID, which could also allow wins without requiring users to renovate their code to allow full file-by-file parallelism.

We look forward to seeing how fast you can go!

@DanielRosenwasser DanielRosenwasser added the Discussion Issues which may not have code impact label Jun 20, 2024
@canonic-epicure
Copy link

Hi,

Would this improvement potentially lead to a long aniticipated solution for #35822 ?

I found it very discouraging, that TypeScript is currently fragmented into 2 languages. The 1st one is TypeScript itself and 2nd is TypeScript+declaration files. The program, valid in the 1st language, is not valid in the 2nd. It seems this is currently not recognized as a problem by TypeScript team, by its very frustrating for users that try to do anything beyond the simplest things in type system, and then publish that as a library.

@robpalme
Copy link
Author

The general answer is no, the work on Isolated Declarations does not change the expressiveness of declaration files. They are unrelated goals. Campaigning for that goal is best done on #35822

However if you only care about #35822 because you don't like the strictness of TS error checking changing when you toggle declaration emit, then sure, enabling isolatedDeclarations (perhaps along with Sheetal's latest fixes that are landing in TS 5.6) should mean that difference goes away. But only because you've enabled an even stricter mode in the first place 🙃

@canonic-epicure
Copy link

Ok, thank you for the explanations.

@DaniGuardiola
Copy link

Hello there, is there a timeline for the option being available in tsconfig.json? Thank you, this is exciting :)

@robpalme
Copy link
Author

robpalme commented Jul 2, 2024

Isolated Declarations can be enabled in tsconfig.json in TypeScript 5.5 which was released two weeks ago.

@jakebailey
Copy link
Member

I think they may mean the json schema on schemastore?

@DaniGuardiola
Copy link

Yeah, I think that's what tripped me up. It might have been available, but my IDE's schema for the config file is telling me it's not a valid option. It is confusing, but I assume it will be updated soon, at some point.

@jakebailey
Copy link
Member

It's technically a third party and just needs to be updated by someone: https://github.com/SchemaStore/schemastore/blob/master/src/schemas/json/tsconfig.json

@DaniGuardiola
Copy link

FWIW, I also can't find any mention of it in the (official?) docs for tsconfig: https://www.typescriptlang.org/tsconfig/

@jakebailey
Copy link
Member

jakebailey commented Jul 8, 2024

The option is now on https://www.typescriptlang.org/tsconfig/#isolatedDeclarations, and will be on schemastore once a PR is sent to copy the schema updates from microsoft/TypeScript-Website#3175 (to both tsconfig and jsconfig schemas).

@jakebailey
Copy link
Member

Schemastore should be updated now.

@aminpaks
Copy link
Contributor

aminpaks commented Sep 5, 2024

@robpalme, do you happen to have a link to the conversation about improving the DX of this feature? I remember at some point folks were discussing to have an internal type such as auto so the language server, and vscode can update the explicit type annotations automatically when they change, and avoid requiring the developer to manually update them.

Given this code:

export function test(): { success: boolean } {
  return {success: Math.random()};
}

and when the logic is updated to the follow:

export function test(skip = false): { success: boolean } {
  if (skip) {
    return {success: false, skipped: true};
  }
  return {success: Math.random()};
}

the developer doesn't have to manually update the type annotation to:

export function test(skip = false): { success: boolean; skipped?: boolean } {
  if (skip) {
    return {success: false, skipped: true};
  }
  return {success: Math.random() > 0.5};
}

something like an internal type that vscode can recognize, and update its generic type automatically?

export function test(): auto<{ success: boolean }> {
  return {success: Math.random()};
}

@dragomirtitian
Copy link
Contributor

dragomirtitian commented Sep 18, 2024

@aminpaks

We have actually thought about having something like this there is a problem though. Type equality is not exactly easy to define. The possible definitions we came up with had issues:

  1. We could define equality as be assignable in both directions, but this can produce false positives for optional properties, so it wouldn't be good enough.
  2. We can define it as being exactly the same type written in the same way. The problem is TypeScript changes it's internal representation of types from version to version. A functionally equivalent type could be written in different ways. This means there is a substantial risk that types produced with one version are not going to be the same as another version. This would lead to a lot of churn when you upgrade your version of TS, so not a great solution either.

Personally I wouldn't mind 2. But the bugs about breaking changes regarding this would be legion. People would default to using this without understanding this potential risk and we can't exactly make everyone sign a waiver that they understand the tradeoffs 😅

@aminpaks
Copy link
Contributor

aminpaks commented Sep 19, 2024

@dragomirtitian,

We're thinking about enabling this mode for all of our composite projects at Shopify, and we believe without having a tooling to automatically update the explicit type annotations, we'll introduce frictions in our developers' workflow.

Honestly I don't mind the option 2, and what you describe in terms of TypeScript version, and the breaking changes may be expected. Would you provide an example of these types?

I wish we could keep the d.ts files next to the source, and commit them to the source control. That would eliminate the need to have the explicit type annotation in the source code, and the type-checker could read that to type check, and the benefits are:

  1. Faster read, parse, and generating AST from type declaration files rather than the source code (the source files can be parsed separately for type checking in parallel)
  2. Allows to keep the source files clean
  3. Eliminate the need to type annotate complex types in the source using a syntactic sugar to auto update

This is crucial as some of the type annotations are extremely hard to maintain (such as Zod's schemas).

We would appreciate your thoughts. 🙏

@frehner
Copy link

frehner commented Oct 22, 2024

If I wanted to turn on --isolatedDeclarations, but am using a class (which, in the explainer, is described as not-yet-supported), is there anyway to ignore/expect-error it? Otherwise, it doesn't seem like I can adopt this feature until it has full support?

TS Playground link

const internals: unique symbol = Symbol('internals');

class MyClass extends HTMLElement {
    // @ts-expect-error this doesn't work; TS still has an error on the next line and won't compile?
    [internals]: ElementInternals;

    constructor() {
        super();
        this[internals] = this.attachInternals();
    }
}

@jakebailey
Copy link
Member

IIRC, #60052 addresses that case?

@bradzacher
Copy link
Contributor

bradzacher commented Oct 30, 2024

It would be great if there was a guide to codemodding your codebase to turn it on.

Doing some research it looks like you can use ts-fix to do it, but I'm not sure of the correct error codes / arguments.
For example I can see at least these error codes: 9007, 9008, 9010, 9011, 9013, 9015, 9016, 9017, 9038 -- is using ts-fix -e 9009 -e 9008 ... the best way to do it?

Is there another tool that can be used?
I've noticed that ts-fix isn't great at large scale and requires some effort to bend it to work.

I'd love it if there was more information here about how to move an existing codebase onto the option!

@JavaScriptBach
Copy link

I've been really keen to try --isolatedDeclarations in my company's codebase, now that Typescript 5.6 implemented --noCheck.

However, the biggest blocker I've run into are libraries that take advantage of TypeScript's type inference to construct complex types from objects at runtime. Some examples would be Zod and Mongoose.

Code like this will not compile under --isolatedDeclarations unless we declare the type of UserObj:

import { z } from "zod";

// error: UserObj must have its type declared
const UserObj = z.object({
  username: z.string(),
});

export type User = z.infer<typeof UserObj>;

This makes sense given the tradeoffs of --isolatedDeclarations, but declaring UserObj results in a messy and hard-to-understand type. It also kind of defeats the purpose of zod, which is to define a runtime schema validator and have the type created from it so it is always in sync.

A similar error happens when attempting to use Mongoose's InferSchemaType.

Will --isolatedDeclarations support these semantics? If not, is there a way to write a typed runtime schema validation library that is compatible with --isolatedDeclarations?

@jakebailey
Copy link
Member

There is not really a good way, besides using code generation or omitting the type and then using a quick fix to write it out for you instead.

isolatedDeclarations is at odds with inference because it effectively removes all inference besides simple cases. Anything more than that, and external tools effectively need to write a type checker.

Perhaps there's some changes to declaration files which would leave those sort of expressions in (allowing what you've written at the cost of more processing later), but as it stands right now, declaration files don't contain anything other than type declarations.

@eventualbuddha
Copy link

What I’d like to see as a version of isolatedDeclarations is to enforce it only for bindings that are exported at the package level. If a binding is not exported by a package root, I don’t really care whether there’s ever a .d.ts file written that contains its type. This could be done such that there is one .d.ts file per .ts file as there is today, just with those bindings omitted. Or it could be done such that the type declarations are all bundled together into a single .d.ts file as various tools do with .js today.

I’m motivated to do some work on this, but I’m unsure whether there’s some feature of TypeScript’s existing tooling or JS modules that would make this especially tricky. In particular, I want to avoid doing any but the most trivial type inference. For prior art on this, I’ve been looking at Oxc and its isolated_declarations crate, in particular with the strip_internal option.

I’m curious to hear others’ ideas on this topic, and whether or not it’s feasible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Discussion Issues which may not have code impact
Projects
None yet
Development

No branches or pull requests