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

Allow type-only named re-exports along with assigned export in the same file #38866

Open
5 tasks done
parzhitsky opened this issue May 31, 2020 · 5 comments
Open
5 tasks done
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@parzhitsky
Copy link

parzhitsky commented May 31, 2020

Search Terms

allow export types assigned =

Suggestion

The suggestion is to allow compilation of files that do both export = and export type {}.

Use Cases

For modules that target both TypeScript and CommonJS usage it would allow consolidating all exports for both module systems in one file (albeit with the requirement of esModuleInterop being true).

Currently, in order to do that I would have to utilize @ts-ignore (see parzh/xrange#69 for details).

Examples

/person.ts:

import personValue from "./person-value";

export = personValue;

export type { default as PersonType } from "./person-type";

/person-type.ts:

export default interface Person { name: string; }

/person-value.ts:

export default { name: "John Doe" } as import("./person-type").default;

Usage:

/usage.ts:

import personValue from "./person";
import personValue, { PersonType } from "./person";
import type { PersonType } from "./person";
import personValue = require("./person");

/usage.js:

const personValue = require("./person");

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, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Jun 12, 2020
@parzhitsky
Copy link
Author

@RyanCavanaugh any updates on this one? Looks like it got lost in the backlog

@andrewbranch
Copy link
Member

person.ts:

import personValue from "./person-value";

namespace personValue {
  export type PersonType = import("./person-type").default;
}

export = personValue;

usage.ts:

import person = require("./person");
person.name; // string;
const p: person.PersonType = { name: "Andrew" };

Or even, if you prefer

person.ts:

import person from "./person-value";
type person = import("./person-type").default;
export = person;

usage.ts:

import Person = require("./person");
Person.name; // string;
const p: Person = { name: "Andrew" };

I feel like these achieve the kind of bundling you want without mixing the semantics of two different module systems, which are already quite confusing without allowing additional blending.

@parzhitsky
Copy link
Author

parzhitsky commented Nov 14, 2020

Hey, Andrew 👋 Thanks for the response!

This isn’t exactly the kind of bundling that I’m trying to achieve, unfortunately; it’s too “tight” in both examples.

I’d like to keep value and type as two separate entities, so that users have more control over what they use. This preserves a notion of single required “primary” entity (export) and multiple optional “secondary” entities per module.

There’s also an issue with the second example: it’s casing suggests the value of Person is a class, which it is not.

Basically, I want this:

// TypeScript
import value, { Type } from "value";
// JavaScript (CommonJS)
const value = require("value");

… so that value is the same entity in both cases. Is that possible with current state of TypeScript?

@andrewbranch
Copy link
Member

The problem is that import value from "value" is not analogous to const value = require("value"). import value = require("value") is analogous to const value = require("value"). These are two separate module systems, and while there is interoperability that allows you to import one into the other, it’s incredibly confusing to try to reason about mixing the two together on the export side. But the first strategy I demonstrated, where auxiliary types are exported via a namespace merge with the “primary” export, is incredibly common.

I only dropped by here because I was searching for outstanding feedback on type-only imports, so this turned up in my search but wasn’t relevant to what I’m working on. But when I read it, it looked clear to me that a solution already exists, and maybe you could benefit from it.

@parzhitsky
Copy link
Author

Thanks for dropping by 🙂 Feel free to ignore this comment if you don’t want to continue conversation. Your input was nevertheless very valuable, I’ll definitely consider the existing solution.


Okay, let’s start with a blank slate and work from there.

There is a “primary” entity, value, that can be used in JavaScript, particularly in CommonJS. Hence, the const value = require("value") importing construct. This value however can also be used in the TypeScript code. And the most elegant construct for importing it would be import value from "value".

At this point, I could have just use import value = require("value") syntax instead for TypeScript code, or enforce esModuleInterop compiler option, and call it done. Both approaches are OK-ish, but not 100% great for various reasons, so let’s continue.

The preferred syntaxes are indeed incompatible out of the box, but with a bit of clever (I am just avoiding to say “ugly”) merging this can be achieved.

The problem arises when another entity appears,—Type, pure type, TypeScript-exclusive. From JS’s point of view, nothing is changed: const value = require("value"). But TypeScript now should have the ability to import it as well. And since it is a separate entity (and it should be, ‘cause it is separate semantically), it becomes a “secondary” one: import value, { Type } from "value".

Because of the choice of importing construct for CommonJS files (I don’t want some weird stuff like const value = require("value").default or const { default: value } = require("value")), the value must be exported with assignment export syntax, export = value;.

But using export =, unfortunately, leaves no room for other exports in this file. This makes sense for runtime-accessible values, ‘cause they will conflict each other. But for types, that are kind of ephemeral and don’t exist on runtime, this enforcement isn’t really reasonable (which is proven by usage examples, both suggested and real-world).

Hence, the suggestion: allow type-only entities to be exported together with assignment export in one file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
Development

No branches or pull requests

3 participants