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

Ambient Module Declarations for Import Assertions #46135

Open
5 tasks done
sodatea opened this issue Sep 29, 2021 · 6 comments
Open
5 tasks done

Ambient Module Declarations for Import Assertions #46135

sodatea opened this issue Sep 29, 2021 · 6 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@sodatea
Copy link

sodatea commented Sep 29, 2021

Suggestion

πŸ” Search Terms

import assertions, ambient module

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Following the suggestion at #40694 (comment)
Allow projects to declare ambient modules based on the import assertions. Such as:

declare module '*' assert {type: 'css'} {
  const stylesheet: CSSStyleSheet;
  export default stylesheet;
}

πŸ“ƒ Motivating Example

Currently frontend build tools use file extensions and URL query strings to support all kinds of custom modules that are eventually compiled into JavaScript.

For example, in Vite (webpack supports these features in a similar way), we support JS modules importing .module.css files as CSS modules, and adding ?url postfixes to the imported path means we are to import the asset's real URL after bundling.

That is:

// get the exported classes from a CSS module
import classes from './example.module.css'

// Explicitly load assets as URL
import assetAsURL from './asset.js?url'

But:

  1. The extension approach may collide with new browser native features, such as the native CSS modules. So it's better to have a way to differentiate them. Import assertions could help in this case.
  2. The query string approach is not a standard.
    • It alters the standard semantics of an import statement
    • Different tools varies on their choices of queries
    • Modules with such syntaxes won't be able to run separately without the specific bundler
    • So, we hope to migrate away from this approach and base the feature upon a standard. Import assertions seems to be the way to go.

One thing that blocks us from doing so is the ability to declare ambient modules based on import assertions in TypeScript.

We can easily have type definitions for files based on their file extensions and query strings. In vite we have: https://github.com/vitejs/vite/blob/b690810f555549e9eed4b03600b27fe7649d6b07/packages/vite/client.d.ts

But currently, I don't see a way to migrate these type definitions when we move to import assertions.

πŸ’» Use Cases

  1. The abovementioned frontend build tools.
  2. Type definitions for browsers' built-in CSS & JSON modules support. We can have declare module '*' assert {type: 'json'} and declare module '*' assert {type: 'css'} in the DOM lib.
@DanielRosenwasser DanielRosenwasser added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Sep 30, 2021
@justinfagnani
Copy link

Thanks for opening this @sodatea!

I think the important point with import assertions is that for the standard assertions (type: 'css' and type: 'json') it's the assertions themselves and nothing about the specifier that determine the type of module. (well, technically it's the MIME type, but if the MIME type and assertion disagree the module will fail, so we can ignore that case)

So for native module types it only makes sense to declare ambient modules by assertion, never by name.

@NuckChorris
Copy link

NuckChorris commented Nov 9, 2021

Just wanted to mention that this would also be a boon for stuff like imagetools which provide complex types currently via query params, but could potentially use assertions instead (and have much better syntax and types as a result!)

@jakearchibald
Copy link

jakearchibald commented Nov 22, 2023

Thoughts on this, following discussion in #56359

Import attributes are a great way to tell a bundler how to interpret/preprocess a file being imported. Eg "load this as plain text", "load this as an ImageBitmap", "bundle this file and give me a URL to it".

The same file may be imported using different types:

import anImage from './icon.png' with { type: 'rollup-ImageBitmap' };
import aURL from './icon.png' with { type: 'rollup-bundled-url' };
import aBase64URL from './icon.png' with { type: 'rollup-base64-url' };

This is much better than the current pattern bundlers/TypeScript uses, where anything ending .png is assumed to be consumed in a particular way, or uses some kind of module prefix/suffix.

Some types, such as { type: 'json' } and { type: 'css' }, may have built-in support on some platforms. It should be possible to use these in conjunction with custom bundler types.

In a case like:

import aDictionary from './styles.css' with { type: 'rollup-css-module' };
import source from './styles.css' with { type: 'rollup-text' };

The build tool may wish to create a definition file specific for styles.css when it's loaded with { type: 'rollup-css-module' }, so it exposes CSS module entries specific to that file, probably by generating a styles.css.d.ts of sorts.


I don't mind how the use-cases are solved, but here are some loosely-held thoughts/ideas.

I know there are two types of .d.ts file, but I cannot remember their official names, and can't find a page in the TypeScript docs that distinguishes between the two. So here's the definitions I'm using for this post:

  • sidecar definition file - this is a .d.ts file that describes the exports of a particular file. This is the mode that TypeScript uses when a .d.ts contains an import or export statement.
  • ambient definition file - this is a .d.ts file that applies across the whole project. This is the mode that TypeScript uses when a .d.ts does not contain an import or export statement.

Taking from #56359, I assume this syntax is for ambient definition files:

declare module "*" with { type: "css" } {
    declare const _default: CSSStyleSheet;
    export default _default;
}

Open question, would it be valid to use types in the above? For example:

declare module "*" with { type: "css" | "scss" } {
    declare const _default: CSSStyleSheet;
    export default _default;
}

And I assume this syntax is for use in sidecar definition files:

declare with { type: "css" } {
  export const header: string;
  export const footer: string;
  export const button: string;
}

It feels like exports outside one of these blocks should only apply to imports without attributes.

I know this is hand-wavy and incomplete, but the process of picking a definition for an import with attributes could be something like:

  1. If there's a sidecar definition file with a matching declare with block, use those types.
  2. Otherwise, if there's an ambient definition file with a matching declare … with, use those types.
  3. Otherwise, if the attributes are of a known platform type (json, css), use those types.
  4. Otherwise, there are no types for this import.

@justinfagnani
Copy link

For non-ambient declarations I think we'll also need a way to specify that there's no support for an import with no type attribute, as is true for CSS and JSON.

@jakearchibald
Copy link

declare with undefined never? 🫀

@fatcerberus
Copy link

I'd just like to point out that "sidecar" vs. "ambient" .d.ts is not a distinction that exists per se - as far as TS is concerned, a .d.ts is always a companion to a corresponding JavaScript file. What you're calling an "ambient" declaration simply describes a .js file that runs as a script (i.e. in global scope, like classic <script> tags) rather than as a module.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants