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

[request] allow export type * from #37238

Closed
3 of 5 tasks
bluelovers opened this issue Mar 6, 2020 · 24 comments · Fixed by #52217
Closed
3 of 5 tasks

[request] allow export type * from #37238

bluelovers opened this issue Mar 6, 2020 · 24 comments · Fixed by #52217
Assignees
Labels
Fix Available A PR has been opened for this issue Needs Investigation This issue needs a team member to investigate its status. Suggestion An idea for TypeScript

Comments

@bluelovers
Copy link
Contributor

bluelovers commented Mar 6, 2020

Search Terms

export type

Suggestion

TS1383: Only named exports may use 'export type'.

allow export type * from

Use Cases

https://github.com/bluelovers/ws-ts-type/blob/master/packages/ts-type/index.ts

export type * from './lib';
export type * from 'typedarray-dts';

Examples

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, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@RyanCavanaugh RyanCavanaugh 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 Mar 10, 2020
@adjerbetian
Copy link

Any news on this topic?

@aminya
Copy link

aminya commented Mar 1, 2021

In addition to all the usecases that export type has, not having it is an inconsistency in the TypeScript language that should be fixed.

P.S: the number of upvotes and likes shows the community opinion. I think "Awaiting more feedback" label should be removed. @RyanCavanaugh

@FezVrasta
Copy link

Just adding my use case here, I maintain a quite popular library (Popper) and I need a way to expose the TS types like I do in Flow (the system used to type the codebase). In Flow I can do export type * from './something' so I'd like to do the same in TS, or have an alternative.

@somebody1234
Copy link

somebody1234 commented Mar 24, 2021

I want to use it to pin dependency versions in Deno... wonder if there's any workaround because I feel like most alternative solutions are very not ergonomic
(edit: it (export *) appears to work... sometimes? i have no clue tbh...)

@parzhitsky
Copy link

Stumbled upon this issue just now.

My use case is that there are definition files *.type.ts, which are intended for exporting only types, and a package entry-point file index.ts which gathers and re-exports all public API from other files (there are some limitations with using *.d.ts initially in source code; unfortunately, I don't remember exactly what's wrong with this approach, but the bottom line is that I have to use custom *.type.ts files):

// thing.type.ts
type Primitive = string | number | boolean; // not exported

export type ThingPropConstraint = Primitive | object;

export interface Thing<Prop extends ThingPropConstraint> {
  prop: Prop;
}
// value.ts
export const value = 42;
// index.ts
export * from "./thing.type";
export * from "./value";

Since *.js output of *.type.ts files will (by convention) always be basically empty, I then remove *.js and *.js.map output files for them, leaving only *.d.ts behind.

Eventually, I expect type exports to be removed from index.js (the output file), leaving only value exports, – but type exports are preserved; and, since the files are gone, this results in an error:

// (output excerpt)
__exportStar(require("./thing.type"), exports); // Error: Cannot find module './thing.type'
__exportStar(require("./value"), exports);

I'd like to enforce TypeScript to remove this export construct:

// index.ts
export type * from "./thing.type";
export * from "./value";

The point of this example is that definition files can define more than one entity, and the information about the exact entities being exporting from a definition file must be preserved in this file (SRP), so re-exporting them must be done using an "export start" kind of export; otherwise, adding another definition will result in constant going back-and-forth between definition files and entry point file.

@parzhitsky
Copy link

@bluelovers Can I ask you to properly fill all the sections in the original comment (search terms, examples, checklist)?

@arthurfiorette
Copy link

Any plans to implement this feature?

@ASDFGerte
Copy link

ASDFGerte commented May 14, 2022

I'm re-exporting type-utilities, keeping the related files as .d.ts (export-statements must be erased). Due to this feature missing, i need to write out every exported type: export type { typeA, typeB, /* ... */ } from './path/file';, and when adding a new type, not forget to add it there too. Luckily import type * exists, so the consumer doesn't have the same problem, but it's a (small) inconvenience.

@nicolo-ribaudo
Copy link

nicolo-ribaudo commented Jun 3, 2022

I just stumbled upon this.

We have a file that only contains type annotations (https://github.com/babel/babel/blob/main/packages/babel-types/src/ast-types/generated/index.ts), and we currently re-export it internally using export * from "..." (https://github.com/babel/babel/blob/d4d08d906a8c5c53cdf5fb0024def91fbfa51698/packages/babel-types/src/index.ts#L101).

When publishing, we strip away TS types and compile to CJS. The types-only files becomes an empty CJS module, and our entry point ends up having some unnecessary code for export * from "...".

Apparently, that empty CJS file causes problems with some tools (babel/babel#14634, https://stackoverflow.com/questions/71045195/cannot-build-run-devserver-cause-of-babel-loader, https://openclassrooms.com/forum/sujet/babel-loader-not-working, kriszyp/lmdb-js#51). It's a bug with those tools, but it would be nice if we could entirely avoid it by marking that re-export as type-only since it's a file that is not needed at runtime.

@andrewbranch
Copy link
Member

@nicolo-ribaudo not sure I follow; export type * from "..." wouldn’t somehow eliminate the empty CJS module that only exported types, right?

@nicolo-ribaudo
Copy link

nicolo-ribaudo commented Jun 27, 2022

It would elide the unnecessary require(), and avoid loading the empty file unnecessarily.

@ghost
Copy link

ghost commented Aug 28, 2022

Any updates on implementing this?

@electrovir
Copy link

Are there any workarounds to this, besides removing the type keyword, that don't require manually enumerating all exports?

@andrewbranch
Copy link
Member

I’m planning to investigate implementing this in the release after 4.9.

@andrewbranch andrewbranch added this to the TypeScript 5.0.0 milestone Oct 20, 2022
@andrewbranch andrewbranch self-assigned this Oct 20, 2022
@andrewbranch andrewbranch added Needs Investigation This issue needs a team member to investigate its status. and removed Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Oct 20, 2022
@justin-calleja
Copy link

My use case for this is that I'm using: https://github.com/acacode/swagger-typescript-api

to generate types from swagger specs. The output file has a combination of types and code and I'd like to be able to export all types in one statement from module wrapping the generated code (so that I don't depend on the generated code directly).

Currently, I have to remember to export any newly generated types. I would like to export type * from "./gen/client.ts" instead.

@andrewbranch
Copy link
Member

I'd like to be able to export all types in one statement

Just to be clear, export type * from "./gen/client" would not just export all the types. It would export everything, just like export * does, as a type-only export. Just like with import, adding the type modifier does not filter what gets imported or exported. It just prevents you from using whatever got imported or exported in a non-type position. Example:

// client.ts
export interface Foo {}
export function foo() {}

// exporter.ts
export type * from "./client";

// consumer.ts
import { Foo, foo } from "./exporter"; // ok
let x: Foo; // ok
let y: typeof foo; // ok
foo(); // error: 'foo' cannot be used as a value because it was exported using 'export type'.

This is why I was hesitant to implement this feature; because people seem to want to read it as “export all the types,” but that’s inconsistent with what any other form of import type or export type does. I now lean toward thinking that the consequences of having that assumption violated are just not that bad. But it’s worth calling out again now that more people are watching this thread (originally explained at #48508 (comment)).

@justin-calleja
Copy link

justin-calleja commented Nov 7, 2022

Just to be clear, export type * from "./gen/client" would not just export all the types. It would export everything, just like export * does, as a type-only export.

@andrewbranch thanks for the clarification. Ok it's true that I had in mind "types only export"... but now that I think about it - "export everything as a type only export" would also work well... in my case at least:

// api-client module:
import { Api } from "./gen/api";

export const client = new Api();

// Anything which is callable code gets exported normally:
export const login = require2XX(client.login.loginCreate);
export const getUser = require2XX(client.user.userList);
// ...

// This is what needs manual intervention every time a new type is generated from spec files:
export type {
  GetUserResBody,
  LoginReqBody,
  Theme,
  User,
} from "./gen/api";
// I would like to replace with:
export type * from "./gen/api";

Anything which is exported by export type * from "./gen/api", is something I don't want to use as a value (don't even generate enums just to avoid accidentally using them as values). If I want something as a value, it gets it's own "proper" export like:

export const getUser = require2XX(client.user.userList);

This is why I was hesitant to implement this feature; because people seem to want to read it as “export all the types,”

I have not thought much about this - but isn't this a "superset"... i.e. even if people think of it as "export all the types" - that seems correct enough. In fact it's export everything but can only use everything as types so you give me the benefit of e.g. using typeof someValue but I can never actually use someValue outside typing stuff. So if I don't know it's "export everything but as types" - I simply won't avail myself to the "extra feature" of actually being able to use values in types.

Does that make sense?

Thanks

@andrewbranch
Copy link
Member

Yes, that’s the right way to think about it. The only downside I can think of is that you’ll see the values in named import completions. It doesn’t sound like a huge deal but a lot of people rely heavily on completions to understand TypeScript’s view of the world, so I can see how it would be confusing to someone who expected only types to show up.

@justin-calleja
Copy link

justin-calleja commented Nov 7, 2022

it would be confusing to someone who expected only types to show up.

@andrewbranch true that - a lot to think about 😓 I am still in favour of adding this; but I'm also not maintaining any of this 😅

Re the confusion concern - explaining the semantics of export type * in error message when user tries using a value exported as type would help but... there's no way I see to reconcile export type * sounding like it exports only types and keeping the consistency re:

people seem to want to read it as “export all the types,” but that’s inconsistent with what any other form of import type or export type does.

If it's any consolation - it makes perfect sense once you know export type * should be read "export everything as a type" rather than "export all types.. as well .. types".

@bakkot
Copy link
Contributor

bakkot commented Nov 7, 2022

@andrewbranch Is there a technical reason that export type * from needs to export typeof-context-only types, or is it just for consistency with export type { x } from where x names a value? And if the latter, is that actually a deliberate choice, or was it just a fallout from how things are implemented?

I know that personally I would never use export type { x } from where x names a value; I'd write type X = typeof x; export type { X } or something instead. It's pretty weird to end up in a situation where you can write typeof x in a type context but not in a value context.

What I'm getting at is: could export type * from be made to differ from export type { x } from? Or, could export type { x } be made to fail when x names a value? (This would be a breaking change, but you might get away with it.) Either would resolve the confusion you're worried about.

(If not, I'd still happily take export type * from even with this possible point of confusion.)

@bakkot
Copy link
Contributor

bakkot commented Nov 7, 2022

Sidebar: if you're implementing export type * from 'mod', it might be worth thinking about export type * as Mod from 'mod' as well (which would, I would imagine, export a namespace named Foo containing all of the types from 'mod') - cf tc39/ecma262#1174.

@andrewbranch
Copy link
Member

And if the latter, is that actually a deliberate choice, or was it just a fallout from how things are implemented?

The first draft of type-only imports filtered symbols down to types. We quickly got feedback that this prevented you from accessing class constructor types:

import type { SomeClass } from "./c";

function makeInstance(SomeClassConstructor: typeof SomeClass): SomeClass {
//                                                 ^^^^^^^^^
//                               Cannot find name 'SomeClass'
  return new SomeClassConstructor();
}

SomeClass is simultaneously a type symbol and a value symbol. As a type, it means the type of an instance of the class. As a value, it is a reference to the class constructor. When you use it in a typeof type, you’re getting the type of the value meaning of the symbol, and you get the type of the constructor. But if the implementation filters things down to just types, that constructor value meaning is gone, so you can only ever reference the instance type. We redesigned the feature based on this feedback, and the implementation turned out to be much simpler after the fact.

Is it possible to make export type * behave differently? Probably, but that would be bad. Consistency matters here not just for consistency’s sake, but because type-only imports and exports work seamlessly with each other. If you do a regular import of a type-only export, it acts exactly the same as if you did a type-only import of a regular export. Adding export * will reuse the existing type-only infrastructure. We’re not going to design and implement a new, distinct feature with a different mental model and leverage it with the same syntax as a very slightly different feature.

it might be worth thinking about export type * as Mod from 'mod' as well

👍 That should come for free.

which would, I would imagine, export a namespace named Foo [sic] containing all of the types from 'mod'

It will be a type-only export of a namespace named Mod containing all the exports from 'mod', same as

import * as Mod from "mod";
export type { Mod };

// or equivalently

import type * as Mod from "mod";
export { Mod };

@bakkot
Copy link
Contributor

bakkot commented Nov 8, 2022

SomeClass is simultaneously a type symbol and a value symbol.

Ah, that makes sense. Thanks for the explanation. It's a bit unfortunate, but given that constraint I see why you have to include values in export type *.

It will be a type-only export of a namespace named Mod containing all the exports from 'mod', same as

Yup, sounds good.

@andrewbranch
Copy link
Member

I would appreciate some help testing #52217 if anyone is interested in trying this out early.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Fix Available A PR has been opened for this issue Needs Investigation This issue needs a team member to investigate its status. Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.