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

Suggestion: Allow ES6 export syntax in namespaces #39865

Open
cruhl opened this issue Aug 2, 2020 · 17 comments
Open

Suggestion: Allow ES6 export syntax in namespaces #39865

cruhl opened this issue Aug 2, 2020 · 17 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

@cruhl
Copy link

cruhl commented Aug 2, 2020

TypeScript Version: [email protected]

Search Terms: namespace, import, export, re-export, module

Code

import { Point } from "./Point";

export type Line = {
  readonly p1: Point;
  readonly p2: Point;
  readonly points?: readonly [Point, ...(readonly Point[])];
};

export namespace Line {
  export { Point } from "~/Point"; // TS says "Export declarations are not permitted in a namespace"
  // `export { Point };` <-- Should also work.
}

type LinePoint = Line.Point;
// ^ Somehow this actually works, i.e. `LinePoint` equals `Point`

Expected behavior:
Exporting from a namespace should mirror ES6 module exports, i.e. export { X }, export { X } from "...", and export * as NS from "..."

The lack of syntax for re-exports makes it very hard to use namespace/type/function merging abilities to represent nice APIs. Here's an example of what a module could look like if this was allowed...

// User.tsx

import { Admin } from "./Admin";
import { Member } from "./Member";

export type User = Admin | Member;
export type Props = { readonly user: User; };

export function User({ user }: Props) {
  return Admin.isAdmin(user) ? <Admin admin={user} /> : <Member member={user} />;
}

export namespace User {
  export { Admin, Member }; // Nice.
  export const isAuthenticated = (user: User): boolean => true;
}

// Home.tsx

import { User } from "./User";

export function Home({ user }: User.Props) {
  return <User.Admin admin={useAdmin()} />; // An example of what this enables.
}

Actual behavior:
TypeScript says "Export declarations are not permitted in a namespace," even though it actually respects the export declaration outside of the namespace.

Playground Link:
https://www.typescriptlang.org/play/#code/HYQwtgpgzgDiDGEAEBBJBvAUEnSIA8YB7AJwBcl4jgoKQkBeJAIhGaRCkutoG5MAvpkyhIsBMgBCGbLgLFyGVEgH8hmKjQr5GSSQDoU+kLyA

Related Issues:
#20273
This issue showcases some of the syntax gymnastics needed to get around this issue.

#4529
#4529 (comment)
More examples of verbose workarounds.

#38041 (comment)
@rbuckton Provides a nice example of what this could look like if enabled.

#38041 (comment)
My comment with another motivating example after the issue was closed.

@cruhl cruhl changed the title Allow ES6 export syntax in namespaces Suggestion: Allow ES6 export syntax in namespaces Aug 17, 2020
@cruhl
Copy link
Author

cruhl commented Aug 17, 2020

@mhegazy Blast from the past...
#4529 (comment)

There's finally an issue tracking this 😅

@cruhl
Copy link
Author

cruhl commented Aug 17, 2020

Here's more "in the wild" code I found showing the syntax noise needed to get around this...

// This file is "./Claim/index.ts"

import * as Details_ from "./Details";
import * as Dashboard_ from "./Dashboard";
import * as Create_ from "./Create";
import * as Admin_ from "./Admin";

import { API } from "../API";

export type Claim = API.GetClaimQuery["getClaim"];
export namespace Claim {
  export const Create = Create_;
  export const Details = Details_;
  export const Dashboard = Dashboard_;
  export const Admin = Admin_;
}

Here's what it could look like...

import { API } from "../API";

export type Claim = API.GetClaimQuery["getClaim"];
export namespace Claim {
  export * as Create from "./Create";
  export * as Details from "./Details";
  export * as Dashboard from "./Dashboard";
  export * as Admin from "./Admin";
}

Or even...

import * as Details from "./Details";
import * as Dashboard from "./Dashboard";
import * as Create from "./Create";
import * as Admin from "./Admin";

import { API } from "../API";

export type Claim = API.GetClaimQuery["getClaim"];
export namespace Claim {
  export { Details, Dashboard, Create, Admin };
}

@cruhl
Copy link
Author

cruhl commented Aug 17, 2020

Here's another relevant discussion: #36684 (comment)

@cruhl
Copy link
Author

cruhl commented Aug 17, 2020

@cruhl
Copy link
Author

cruhl commented Aug 17, 2020

Here's a repo someone created to demonstrate this issue, coincidently I used the same Point example in this issue: https://github.com/danprince/ts-export-types-bug

@cruhl
Copy link
Author

cruhl commented Aug 19, 2020

I just found out this works, but only in ambient contexts, which is bizarre:

declare module "./Line" {
  export namespace Line {
    export { Point };
  }
}

@todd-elvers
Copy link

todd-elvers commented Aug 19, 2020

This is also a feature I've been looking for.

I've done this more times than I'd like to admit:

import * as Handler_ from "./Handler"

export type Connection = ...
export namespace Connection {
  export const Handler = Handler_
}

Just to achieve

const connectionHandler = Connection.Handler.create()

@cruhl
Copy link
Author

cruhl commented Aug 19, 2020

Another way to achieve this without new syntax would be to allow type and value imports to have the same name, as is common in other scenarios...

import * as Connection from "./Connection"; // namespace
import { Connection } from "./Connection"; // type

@cruhl
Copy link
Author

cruhl commented Aug 19, 2020

You can also basically achieve this, but with huge drawbacks, using classes:

import { Point } from "~/Point";

class Line {
  static readonly Point = Point // works, but you can't do `export { Point }` in a namespace 👎 
}

@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 Sep 1, 2020
@douglas-sousa
Copy link

douglas-sousa commented Oct 10, 2020

Sorry if this is the wrong place to ask but namespace will still be a thing to be worked on? I usually encounter deprecation messages when I use it and Storybook sends an error message telling me to configure babel if I want to use it.

Like @cruhl, I also found namespace to be a nice way to organize data and keep it more semantic

// basic example

// Printer.ts
export namespace Printer {
  export type Type = 'laser' | 'LED' | '3D';
  export type Return = { success: boolean, data: object };
}

export interface Printer {
  print (type: Printer.Type): Printer.Return;
}

// MyPrinter.ts
class MyPrinter implements Printer {
  print (type: Printer.Type): Printer.Return {
    // do stuff
    return { success: true, data: {} };
  }
}

@cruhl
Copy link
Author

cruhl commented Jan 12, 2022

@andrewbranch I apologize if tagging you directly isn't the right move, let me know if that's the case, but I was wondering if you could provide some insight since you recently closed another related issue.

This is still something I believe would be a useful change and I'm eager/willing to champion/help in whatever way I can. Any pointers for moving the issue forward? Thanks in advance!

@cruhl
Copy link
Author

cruhl commented Oct 25, 2022

This issue has popped up again:

#51109

@todd-elvers
Copy link

If there's a good reason to not implement this feature I'd love to know about it. I feel as though I've seen this issue pop up a few times and it always gets closed with little-to-no explanation. I suppose if we could come up with a PR then maybe we could get some more in-depth feedback.

@cruhl
Copy link
Author

cruhl commented Oct 25, 2022

This amazingly works...
CleanShot 2022-10-25 at 14 59 05
CleanShot 2022-10-25 at 14 59 00
CleanShot 2022-10-25 at 14 58 53

@cruhl
Copy link
Author

cruhl commented Oct 25, 2022

Wow, still too much syntax, but it's so much better than export import Something = Something_...

CleanShot 2022-10-25 at 15 07 03

@Fryuni
Copy link

Fryuni commented Oct 26, 2023

Another use case is exporting values with keywords as names (like return or try). Those might be generated from an external specification and should keep the original names.

cruhl's workaround works with some modification:

declare namespace WorkflowStepBuilder {
    const _return: (value: string) => WorkflowStep<'return'>;
    export {_return as return};
}

namespace WorkflowStepBuilder {
    function _return(value: string): WorkflowStep<'return'> {
        return {step: 'return', value: value}
    }

    WorkflowStepBuilder.return = _return;
}

Quite a pain to write the generator for such scenarios

@cruhl
Copy link
Author

cruhl commented Nov 7, 2023

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

5 participants