You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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. Ryan Cavanaugh (@RyanCavanaugh)
Reacted by Alexandre Djerbetian, Aden Narkowicz, Manuel Cascallar, John Bernard, 流浪大法师, kenjiru, Maxime, Shaul Goral, Sven Jacobs, 吴乐川 and 5 more
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.
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...)
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.tstypePrimitive=string|number|boolean;// not exportedexporttypeThingPropConstraint=Primitive|object;exportinterfaceThing<PropextendsThingPropConstraint>{prop: Prop;}
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:
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.
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.
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 "...".
Nicolò Ribaudo (@nicolo-ribaudo) not sure I follow; export type * from "..." wouldn’t somehow eliminate the empty CJS module that only exported types, right?
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.
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.tsexportinterfaceFoo{}exportfunctionfoo(){}// exporter.tsexporttype*from"./client";// consumer.tsimport{Foo,foo}from"./exporter";// okletx: Foo;// oklety: typeoffoo;// okfoo();// 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)).
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.
Andrew Branch (@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";exportconstclient=newApi();// Anything which is callable code gets exported normally:exportconstlogin=require2XX(client.login.loginCreate);exportconstgetUser=require2XX(client.user.userList);// ...// This is what needs manual intervention every time a new type is generated from spec files:exporttype{GetUserResBody,LoginReqBody,Theme,User,}from"./gen/api";// I would like to replace with:exporttype*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:
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.
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.
it would be confusing to someone who expected only types to show up.
Andrew Branch (@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".
Andrew Branch (@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.)
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.
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:
importtype{SomeClass}from"./c";functionmakeInstance(SomeClassConstructor: typeofSomeClass): SomeClass{// ^^^^^^^^^// Cannot find name 'SomeClass'returnnewSomeClassConstructor();}
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*asModfrom"mod";exporttype{Mod};// or equivalentlyimporttype*asModfrom"mod";export{Mod};
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
Activity
adjerbetian commentedon Jan 22, 2021
Any news on this topic?
aminya commentedon Mar 1, 2021
In addition to all the usecases that
export typehas, 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. Ryan Cavanaugh (@RyanCavanaugh)
FezVrasta commentedon Mar 1, 2021
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 commentedon 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 commentedon Sep 29, 2021
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 fileindex.tswhich gathers and re-exports all public API from other files (there are some limitations with using*.d.tsinitially 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.tsfiles):Since
*.jsoutput of*.type.tsfiles will (by convention) always be basically empty, I then remove*.jsand*.js.mapoutput files for them, leaving only*.d.tsbehind.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:I'd like to enforce TypeScript to remove this export construct:
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 commentedon Sep 29, 2021
bluelovers (@bluelovers) Can I ask you to properly fill all the sections in the original comment (search terms, examples, checklist)?
arthurfiorette commentedon Dec 9, 2021
Any plans to implement this feature?
ASDFGerte commentedon 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. Luckilyimport type *exists, so the consumer doesn't have the same problem, but it's a (small) inconvenience.nicolo-ribaudo commentedon 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 commentedon Jun 27, 2022
Nicolò Ribaudo (@nicolo-ribaudo) not sure I follow;
export type * from "..."wouldn’t somehow eliminate the empty CJS module that only exported types, right?nicolo-ribaudo commentedon Jun 27, 2022
It would elide the unnecessary require(), and avoid loading the empty file unnecessarily.
electrovir commentedon Oct 17, 2022
Are there any workarounds to this, besides removing the
typekeyword, that don't require manually enumerating all exports?2 remaining items
justin-calleja commentedon Nov 6, 2022
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 commentedon Nov 7, 2022
Just to be clear,
export type * from "./gen/client"would not just export all the types. It would export everything, just likeexport *does, as a type-only export. Just like withimport, adding thetypemodifier 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: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 typeorexport typedoes. 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 commentedon Nov 7, 2022
Andrew Branch (@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:
Anything which is exported by
export type * from "./gen/api", is something I don't want to use as a value (don't even generateenumsjust to avoid accidentally using them as values). If I want something as a value, it gets it's own "proper" export like: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 someValuebut I can never actually usesomeValueoutside 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 commentedon Nov 7, 2022
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 commentedon Nov 7, 2022
Andrew Branch (@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 astypewould help but... there's no way I see to reconcileexport type *sounding like it exports only types and keeping the consistency re: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 commentedon Nov 7, 2022
Andrew Branch (@andrewbranch) Is there a technical reason that
export type * fromneeds to exporttypeof-context-only types, or is it just for consistency withexport type { x } fromwherexnames 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 } fromwherexnames a value; I'd writetype X = typeof x; export type { X }or something instead. It's pretty weird to end up in a situation where you can writetypeof xin a type context but not in a value context.What I'm getting at is: could
export type * frombe made to differ fromexport type { x } from? Or, couldexport type { x }be made to fail whenxnames 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 * fromeven with this possible point of confusion.)bakkot commentedon Nov 7, 2022
Sidebar: if you're implementing
export type * from 'mod', it might be worth thinking aboutexport type * as Mod from 'mod'as well (which would, I would imagine, export a namespace namedFoocontaining all of the types from'mod') - cf tc39/ecma262#1174.andrewbranch commentedon Nov 8, 2022
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:
SomeClassis 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 atypeoftype, 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. Addingexport *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.👍 That should come for free.
It will be a type-only export of a namespace named
Modcontaining all the exports from'mod', same asbakkot commentedon Nov 8, 2022
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 *.Yup, sounds good.
andrewbranch commentedon Jan 13, 2023
I would appreciate some help testing #52217 if anyone is interested in trying this out early.