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

Support a way to export type X and * as X as namespace with values and types #51975

Closed
5 tasks done
patroza opened this issue Dec 20, 2022 · 13 comments · Fixed by #53387
Closed
5 tasks done

Support a way to export type X and * as X as namespace with values and types #51975

patroza opened this issue Dec 20, 2022 · 13 comments · Fixed by #53387
Assignees
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue Rescheduled This issue was previously scheduled to an earlier milestone

Comments

@patroza
Copy link

patroza commented Dec 20, 2022

Suggestion

🔍 Search Terms

  • export
  • namespace
  • 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

I would like to be able to easily export a whole module including types and values, and at the same time export a type with the same name.

📃 Motivating Example

I want to have a single import of Something, which represents both a type, and a namespace containing all types and values of the Something module.

Usage:

import { Something } from "./prelude"
export const myValue: Something<string> = Something.of("abc")
export type MyType = Something.SubType<string>

prelude.ts:

Option 1:

import * as S from "./Something"

// currently causes only the values of Something to be accessible via `Something.x`, but not the types like `Something.SubType`
export * as Something from "./Something"
export type Something<A> = S.Something<A>

The limitation seems strange. Perhaps just a bug?

Option 2:

import * as S from "./Something"

export namespace Something {
   // currently causes `Export declarations are not permitted in a namespace.ts(1194)`
   export * from "./Something"
}
export type Something<A> = S.Something<A>

The limitation seems arbitrary. We can already export from namespaces. Why not allow * exports for convenience?

Option 3:

// currently causes `Duplicate identifier 'Something'.ts(2300)`
export * as Something from "./Something"
export type { Something } from "./Something"

The limitation seems strange. As we already can create a type and value/namespace with same name, and export them together.

Something.ts:

export interface Something<A> {
   something: A
}

export const of = (a: A): Something<A> => ({ something: a })

export interface SubType<A> {
  subType: A
}

💻 Use Cases

The current approaches either mean I have to use Something.Something<string> and also see this Something. namespace in type errors and on hover in editor. It's verbose, and ugly.

Or I need to work around the issue in either of two ways, both of them require manual syncing of files, or individual exports:

Workaround 1:

Manually create:
Prelude.d.ts:

import * as S from "./Something"
export namespace Something {
  // in d.ts files it causes no error!
  export * from "./Something"
}
export type Something<A> = S.Something<A>

Prelude.js:

export * as Something from "./Something"

Workaround 2: Manually alias all the exports in a namespace:

Prelude.ts:

import * as S from "./Something"
export namespace Something {
   export const of = S.of
   export type SubType<A> = S.SubType<A>
}
export type Something<A> = S.Something<A>
@mikearnaldi
Copy link

It could also be nice to allow for:

export * as Something from "./Something"
export type { Something } from "./Something"

Given that it is already possible to have:

type Something = number;
const Something = "hello";

within a single file as type level symbols and value level symbols can overlap without conflicts

@patroza
Copy link
Author

patroza commented Dec 20, 2022

It could also be nice to allow for:

export * as Something from "./Something"
export type { Something } from "./Something"

Totally, added as Option 3 (I suppose all 3 limitations lifted would be awesome)

@bakkot
Copy link
Contributor

bakkot commented Dec 20, 2022

This looks like it's related to #37238, maybe?

@patroza
Copy link
Author

patroza commented Dec 20, 2022

This looks like it's related to #37238, maybe?

Related, but not duplicate, I believe.
Allowing “export type *” would not resolve this issue.

@andrewbranch
Copy link
Member

Option 1:
...
currently causes only the values of Something to be accessible via Something.x, but not the types like Something.SubType

I’m not sure, but I think this might just be a bug.

@andrewbranch andrewbranch added the Needs Investigation This issue needs a team member to investigate its status. label Dec 21, 2022
@andrewbranch andrewbranch self-assigned this Dec 21, 2022
@andrewbranch andrewbranch added this to the TypeScript 5.0.1 milestone Dec 21, 2022
@mikearnaldi
Copy link

Option 1:
...
currently causes only the values of Something to be accessible via Something.x, but not the types like Something.SubType

I’m not sure, but I think this might just be a bug.

Probably is, even if that is solved though it I believe it won't be enough to resolve this proposal fully, the reason being Something<A> and S.Something<A> will still be separated types (even though representing the same). That means any type expressed in terms of S.Something<A> will not by default appear as Something<A> when importing like:

import { Something } from "./prelude"
// this would work
export const myValue: Something<string> = Something.of("abc")

// this would not infer as Something<A>
export const myValue = Something.of("abc")

@patroza
Copy link
Author

patroza commented Dec 21, 2022

Probably is, even if that is solved though it I believe it won't be enough to resolve this proposal fully, the reason being Something<A> and S.Something<A> will still be separated types (even though representing the same). That means any type expressed in terms of S.Something<A> will not by default appear as Something<A>

It depends, I didn’t confirm this case, but afaik typescript will try to stick to an Alias when it is found “earlier”. That said, ideal would be each of the cases solved :)

@mikearnaldi
Copy link

Probably is, even if that is solved though it I believe it won't be enough to resolve this proposal fully, the reason being Something<A> and S.Something<A> will still be separated types (even though representing the same). That means any type expressed in terms of S.Something<A> will not by default appear as Something<A>

It depends, I didn’t confirm this case, but afaik typescript will try to stick to an Alias when it is found “earlier”. That said, ideal would be each of the cases solved :)

Never seen typescript do anything of the sort, the closest available symbol is usually taken not considering aliases. Try the same in a single file importing '* as S' and defining locally a new type alias, you'll see that TS doesn't pick it up (and it really shouldn't otherwise it would need to resolve all the aliases to know what points to what)

@patroza
Copy link
Author

patroza commented Jan 28, 2023

Some related fixes have gone in TS 5.0 beta it seems: #37238
Fingers crossed for this one :)

@andrewbranch andrewbranch added Rescheduled This issue was previously scheduled to an earlier milestone Bug A bug in TypeScript and removed Needs Investigation This issue needs a team member to investigate its status. labels Mar 20, 2023
@typescript-bot typescript-bot added the Fix Available A PR has been opened for this issue label Mar 20, 2023
@patroza
Copy link
Author

patroza commented Mar 29, 2023

hi @andrewbranch this is great, thanks!
This case does not work btw:

export * as Something from "./Something"
export type { Something } from "./Something"

It will complain

Duplicate identifier 'Something'.

@andrewbranch
Copy link
Member

Yeah. Alias declarations do not merge with each other. That one is not intended to work.

@patroza
Copy link
Author

patroza commented Mar 29, 2023

Yeah. Alias declarations do not merge with each other. That one is not intended to work.

@andrewbranch why don't they?
export const A = 1
export type A = string
does merge

@andrewbranch
Copy link
Member

Those aren’t alias declarations; those are local declarations that are also exported. To be honest, I don’t know why alias symbols can’t merge, when an alias and a local can merge provided the meanings don’t conflict. But the restriction is deeply encoded into the symbol merging architecture and is definitely intentional. Changing that, even if there were no soundness or performance downsides, would be a huge lift. I can’t really give a justification that’s satisfying from a user perspective (though maybe there is one), but I can tell you after working on #50455, there are so many things implementation-wise that would be so much harder if aliases were allowed to merge with each other.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue Rescheduled This issue was previously scheduled to an earlier milestone
Projects
None yet
5 participants