Typescript: Suggestion: Allow ES6 export syntax in namespaces

Created on 2 Aug 2020  路  10Comments  路  Source: microsoft/TypeScript

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:
https://github.com/microsoft/TypeScript/issues/20273
This issue showcases some of the syntax gymnastics needed to get around this issue.

https://github.com/microsoft/TypeScript/issues/4529
https://github.com/microsoft/TypeScript/issues/4529#issuecomment-140932811
More examples of verbose workarounds.

https://github.com/microsoft/TypeScript/issues/38041#issuecomment-616807133
@rbuckton Provides a nice example of what this could look like if enabled.

https://github.com/microsoft/TypeScript/issues/38041#issuecomment-662142857
My comment with another motivating example after the issue was closed.

Awaiting More Feedback Suggestion

Most helpful comment

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 };
}

All 10 comments

@mhegazy Blast from the past...
https://github.com/microsoft/TypeScript/issues/4529#issuecomment-231476618

There's finally an issue tracking this 馃槄

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 };
}

Here's another relevant discussion: https://github.com/microsoft/TypeScript/issues/36684#issuecomment-627543157

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

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

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

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()

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

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 馃憥 
}

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: {} };
}
}
````

Was this page helpful?
0 / 5 - 0 ratings