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

feat(41825): JSDoc equivalent of import * #57207

Merged
merged 32 commits into from
Mar 26, 2024
Merged

Conversation

a-tarasyuk
Copy link
Contributor

@a-tarasyuk a-tarasyuk commented Jan 28, 2024

Fixes #22160
Fixes #41825
Fixes #41825 (comment)

@typescript-bot
Copy link
Collaborator

Looks like you're introducing a change to the public API surface area. If this includes breaking changes, please document them on our wiki's API Breaking Changes page.

Also, please make sure @DanielRosenwasser and @RyanCavanaugh are aware of the changes, just as a heads up.

@DanielRosenwasser
Copy link
Member

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jan 31, 2024

Heya @DanielRosenwasser, I've started to run the tarball bundle task on this PR at 0a20ad3. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jan 31, 2024

Hey @DanielRosenwasser, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/159723/artifacts?artifactName=tgz&fileId=D02528D51446E4533EE8387E4D86BA71B3A7D3FC035CD42935DA2197A5F1290D02&fileName=/typescript-5.4.0-insiders.20240131.tgz"
    }
}

and then running npm install.


There is also a playground for this build and an npm module you can use via "typescript": "npm:@typescript-deploys/pr-build@5.4.0-pr-57207-3".;

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Jan 31, 2024

Something we should test

/**
 * @importType
 * as Foo from "foo"
 */

and

/**
 * @importType {
 *   A,
 *   B,
 *   C,
 * } from "foo"
 */
@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Jan 31, 2024

In our design meeting, I brought up some of the discussion on Twitter around the tag name.

I think the team is up for just calling it @import.

Something that came up was the use of type modifiers on import specifiers.

/**
 * @import { type Foo } from "foo"
 */

The preference was to disallow them for now, even though it might make copy/pasting from TS to JS easier. The idea was can always relax that restriction.

Another two things that I do want us to test:

  1. Auto-imports - whether it works, where they get inserted in an import list, etc.
  2. Declaration file generation - precisely how a /** @import */ comment translates into a TS import within a .d.ts file.

As raised above, multi-line should just work:

/**
 * @import {
 *   A,
 *   B,
 *   C,
 * } from "foo"
 */

However, I don't know exactly how this should be handled, and I am curious to hear thoughts from @sandersn (who can also help out with where the leading * logic lives in our JSDoc implementation).

/**
 * @import
 * as Foo from "foo"
 ^
 what is this?
 */

As I write this up, it really makes me realize like the devil is in the details. 😅

@a-tarasyuk
Copy link
Contributor Author

a-tarasyuk commented Feb 1, 2024

As raised above, multi-line should just work:

/**
 * @import {
 *   A,
 *   B,
 *   C,
 * } from "foo"
 */

@DanielRosenwasser I think the scanner can handle this case. However, I think we need to change the naming for this option :).

setInJSDocType,

if (inJSDocType && !asteriskSeen && (tokenFlags & TokenFlags.PrecedingLineBreak)) {
// decoration at the start of a JSDoc comment line
asteriskSeen = true;
continue;
}

@a-tarasyuk
Copy link
Contributor Author

Auto-imports - whether it works, where they get inserted in an import list, etc.

@DanielRosenwasser The existing auto-import logic will add @param { import("./a").A } for the following case., Should we consider changing this logic to use @import?

// @Filename: /a.ts
export default interface A { }

// @Filename: /test.js
/**
 * @param { A/**/ } a
 */
export function f(a) { }
@a-tarasyuk
Copy link
Contributor Author

a-tarasyuk commented Feb 6, 2024

/**
 * @import
 * as Foo from "foo"
 ^
 what is this?
 */

@DanielRosenwasser It's a tricky case because the first * in the JSDoc is a decoration. So, if we handle import tag like other tags, the correct way to use multiline import * should be...

/**
 * @import
 * * as Foo from "foo"
 */
@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Feb 6, 2024

Should we consider changing this logic to use @import?

I would say so - however, in the interest of keeping this PR smaller, you might want to do a follow-up PR instead. @andrewbranch might have thoughts here.


Regarding the ambiguity with *, I am okay with either decision. My suggestion is to say that the next token following a JSDoc import has to occur on the same line. @sandersn might have thoughts here.

@a-tarasyuk
Copy link
Contributor Author

Declaration file generation - precisely how a /** @import */ comment translates into a TS import within a .d.ts file.

@DanielRosenwasser These imports are currently transformed into ImportTypeNode ({import("./types").A}). Is it better to translate import tags as top-level declarations within the checker?

const result = resolver.getDeclarationStatementsForSourceFile(sourceFile, declarationEmitNodeBuilderFlags, symbolTracker, bundled);

@andrewbranch
Copy link
Member

andrewbranch commented Feb 7, 2024

Since these have type-only import semantics in JSDoc, I would have assumed they would translate to type-only import declarations in the .d.ts file. I’m not sure it actually matters though.

We have to remember that JS-based declaration emit works completely differently from TS-based declaration emit—instead of walking the source file, it walks the symbol table and just synthesizes types for stuff. So as far as the declaration emitter is concerned, this feature really changes nothing. It’s just one more way to give visible symbols a type, but the declaration emitter already doesn’t know or care how anything got its type when working from JS.

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not done reviewing, but I found that I wrote some requests for tests last time I looked at this and forgot to post them.

src/compiler/parser.ts Show resolved Hide resolved
src/compiler/parser.ts Outdated Show resolved Hide resolved
src/compiler/parser.ts Outdated Show resolved Hide resolved
src/compiler/transformers/declarations.ts Outdated Show resolved Hide resolved
src/services/completions.ts Outdated Show resolved Hide resolved
src/services/refactors/convertImport.ts Outdated Show resolved Hide resolved
src/services/stringCompletions.ts Outdated Show resolved Hide resolved
@sandersn
Copy link
Member

sandersn commented Feb 7, 2024

Regarding the ambiguity with *, I am okay with either decision. My suggestion is to say that the next token following a JSDoc import has to occur on the same line. @sandersn might have thoughts here.

I'm also OK with any decision, and I think strict no-newline one is a good place to start except that it would require special-casing parseTag, which currently uses

const tagName = parseJSDocIdentifierName(/*message*/ undefined);
const indentText = skipWhitespaceOrAsterisk();

So I'd personally leave it as interpreting a leading asterisk as decoration:

/**
 * @import
 * { foo, bar }
 * from "multiline.js"
 */

equivalent to import { foo, bar } from 'multiline.js'. I don't think it's worth any additional code. Just test that

/**
 * @import
 * * as multiline from 'multiline.js'
 */

works.

src/compiler/checker.ts Outdated Show resolved Hide resolved
Copy link
Member

@weswigham weswigham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine? @andrewbranch should give the type-only behavior a once-over, though.

PR Backlog automation moved this from Waiting on author to Needs merge Mar 22, 2024
@andrewbranch
Copy link
Member

I was skimming for a test like this and I didn’t see one, so let me know if I missed it:

// @declaration: true
// @emitDeclarationOnly: true
// @checkJs: true
// @allowJs: true

// @filename: 0.ts
export default interface Foo {}
export interface I {}

// @filename: 1.js
/** @import Foo, { I } from './0' */

/**
 * @param {Foo} a
 * @param {I} b
 */
export function foo(a, b) {}

Is this input syntax valid, and does it emit valid declaration file syntax? The input syntax should be valid in my opinion, but if it wasn’t handled specifically, it may emit invalid syntax on the output:

import type Foo, { I } from './0';
//     ^^^^^^^^^^^^^^^
// A type-only import can specify a default import or named bindings, but not both.(1363)
@sandersn sandersn merged commit 2de69b0 into microsoft:main Mar 26, 2024
25 checks passed
PR Backlog automation moved this from Needs merge to Done Mar 26, 2024
timocov added a commit to timocov/dts-bundle-generator that referenced this pull request Apr 1, 2024
@bcomnes
Copy link

bcomnes commented Jun 4, 2024

I love this feature! I've noticed though, that if you import a type with the new syntax, and then only use that type inside of another typedef (used or unused in the js file), you get a ts6133 "is declared but its value is never read." warning, despite the imported type resolving and the new typedef being declared properly. It only counts read state when something is assigned the type directly.

@bcomnes
Copy link

bcomnes commented Jun 5, 2024

Hrmmm you are right. I'm having a hard time coming up with a minimal reproducible example. I think it may be related to cycles in the type import, but also related to usage of the cycling type. I'll report back if I can pin it down.

@acidoxee
Copy link

My apologies if this has already been discussed, but it seems the new @import syntax doesn't support import attributes, especially the resolution-mode one that enables @typedef to resolve imports correctly regardless of the type of module being used.

For additional details, my codebase is mostly ESM, but some parts still have to be CJS for compatibility reasons with "old" tools. In those CJS parts, I previously imported types from the ESM part like this:

/** @typedef {import('../path/to/some/ESModule.js', { with: { 'resolution-mode': 'import' } }).MyType} MyType */

I have tried migrating to the new @import tag with an equivalent syntax, but it seems the import attribute is being ignored:

/** @import {MyType} from '../path/to/some/ESModule.js' with { 'resolution-mode': 'import' } */

Using the above syntax yields this error message:

The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'. Consider writing a dynamic 'import("../path/to/some/ESModule.js")' call instead.
To convert this file to an ECMAScript module, change its file extension to '.mjs', or add the field "type": "module" to '/CJSpart/package.json'. ts(1479)

Am I wrong, or is this simply not available yet for @import? Thanks 🙏

@a-tarasyuk
Copy link
Contributor Author

@acidoxee Can you create a new issue? I'll take a look at it.

@acidoxee
Copy link

Sure @a-tarasyuk, here you go! #58955
I hope the format of the issue is OK, I couldn't create a TS playground with several files because of #52666.

@zheung
Copy link

zheung commented Jun 22, 2024

@bcomnes I can't reproduce that behavior; here's my attempt in the playground: https://www.typescriptlang.org/play/?noUnusedLocals=true&ts=5.6.0-dev.20240605&filetype=js#code/PTAEAEDsHsFVIK4GcCmATAMtAxgQwDZIBcoALgE4IoCwAUHQJYC2ADtOaaAEQBmSXAbjp1gAKlERmbDqADeoAOrkGpFADEG+FAHkWpBtEhJQAX1A9y0Jtz5dQo4MNohQ8bFaYpI+yAHNzDAAeKMakABYooCjkluQiYGIS4KQAnizoKDxySirqmjp6BkZmCmra9sBAA

If you can show an example, could you open a new issue? Thanks!

@andrewbranch maybe i can reproduce this with my case:

Version: 1.91.0-insider
Commit: 4bbebd8af922ed3b5a7a14eb0848c04918454c38
Date: 2024-06-21T03:34:42.273Z
Browser: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Code-Insiders/1.91.0-insider Chrome/122.0.6261.156 Electron/29.4.0 Safari/537.36

types.d.ts

export type TestType = {
	route: string;
	method?: string | undefined;
	handle: Function;
	upload?: boolean | undefined;
	option?: Object | undefined;
};

test1.js

/** @import { TestType } from './types.d.ts' */

export default async function doSomething() {
	/** @type {TestType[]} */
	const types = [];

	return types;
}

test2.js

/** @import { TestType } from './types.d.ts' */

export default {
	/** @type {TestType[]} */
	types: []
};

screenshot@240622-183722
screenshot@240622-183727
screenshot@240622-183736

update1: reproduce on playground:
https://www.typescriptlang.org/play/?noUnusedLocals=true&ts=5.6.0-dev.20240605&filetype=js#code/PTAEAEDsHsFVIK4GcCmATAMtAxgQwDZIBcoALgE4IoCwAUHQJYC2ADtOaaAEQBmSXAbjp1gAKlERmbDqADeoAOrkGpFADEG+FAHkWpBtEhJQAX1A9y0Jtz5dQo4MNohQ8bFaYpI+yAHNzDAAeKMakABYooCjkluQiYCiB0pxoKDy4CPicsvGgAJBiEuCkAJ4skbJKKuqaOnoGRgDaALpmDrl5peXEoC25JkL0tInJoKnpmZy4SCWQ2OYIc-qGY9AAylYo4Qx+ABQAlHJ0BeIQXRVVqhpaustNrfaOtHnuRpznxgC8vc2Dx+RbBDkSBkMohQYmIA

update2:
i fount it only work when @import before true import, nah after true import or without true import
image

@zheung
Copy link

zheung commented Jun 23, 2024

For the same project as above, I found another problem: tsc doesn't seem to recognize types imported via @import correctly.

screenshot@240623-113727

@jbeaudoin11
Copy link

Hi guys, not sure if i should simply open an issue instead of writing here but is it me or it's not possible to just import all ?

/** @import * from "./types.ts" */
@DanielRosenwasser
Copy link
Member

In general it's not best practice to comment on a closed PR or issue since people stop following as often, and creates noise in the conversation.

To answer your question, you have to create a name for the import:

/** @import * as myTypes from "./types.ts" */
//            ^^^^^^^^^^

JavaScript/TypeScript doesn't support an "import all the things from this module into the current scope" sort of import statement.

@jbeaudoin11
Copy link

Alright, i'm really confused, i swear i used to do this all the time before ESM was a thing but maybe i'm wrong...
Maybe i assumed we could do it since we can do it with export 🤷‍♂️
Thanks !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Backlog Bug PRs that fix a backlog bug
10 participants