Skip to content

Suggestion: Allow ES6 export syntax in namespaces #39865

Open

Description

@cruhl

TypeScript Version: typescript@4.0.0-dev.20200801

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)
Ron Buckton (@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.

Activity

  1. changed the title [-]Allow ES6 export syntax in namespaces[/-] [+]Suggestion: Allow ES6 export syntax in namespaces[/+] on Aug 17, 2020
  2. cruhl commented on Aug 17, 2020

    @cruhl
    Author

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

    There's finally an issue tracking this 😅

  3. cruhl commented on Aug 17, 2020

    @cruhl
    Author

    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 };
    }
  4. cruhl commented on Aug 17, 2020

    @cruhl
    Author

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

  5. cruhl commented on Aug 17, 2020

    @cruhl
    Author

    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

  6. cruhl commented on Aug 19, 2020

    @cruhl
    Author

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

    declare module "./Line" {
      export namespace Line {
        export { Point };
      }
    }
  7. todd-elvers commented on Aug 19, 2020

    @todd-elvers

    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()
  8. cruhl commented on Aug 19, 2020

    @cruhl
    Author

    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
  9. cruhl commented on Aug 19, 2020

    @cruhl
    Author

    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 👎 
    }
  10. douglas-sousa commented on Oct 10, 2020

    @douglas-sousa

    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 Conner Ruhl (@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: {} };
      }
    }
    
  11. cruhl commented on Jan 12, 2022

    @cruhl
    Author

    Andrew Branch (@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!

  12. cruhl commented on Oct 25, 2022

    @cruhl
    Author

    This issue has popped up again:

    #51109

  13. todd-elvers commented on Oct 25, 2022

    @todd-elvers

    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.

  14. cruhl commented on Oct 25, 2022

    @cruhl
    Author

    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

  15. cruhl commented on Oct 25, 2022

    @cruhl
    Author

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

    CleanShot 2022-10-25 at 15 07 03

  16. Fryuni commented on Oct 26, 2023

    @Fryuni

    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

  17. cruhl commented on Nov 7, 2023

    @cruhl
    Author
  18. raythurnvoid commented on Apr 29, 2025

    @raythurnvoid

    At least this should be allowed

    Image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions