Repository navigation
Let type identifiers co-exist with import namespace identifiers #36704
Description
Search Terms
Modules, export, import
Suggestion
It would be convienent if type identifiers could co-exist with import namespace identifiers.
Use Cases
A common pattern is to have a module where the module name is the same as the main type exported by that module.
Examples
Consider this module:
result.ts
export type Result<TError, TValue> =
| { readonly type: "Ok"; readonly value: TValue }
| { readonly type: "Err"; readonly error: TError };
export function Ok<TValue>(value: TValue): Result<never, TValue> {
return { type: "Ok", value };
}
export function Err<TError>(error: TError): Result<TError, never> {
return { type: "Err", error };
}
// .. more functions to work with the Result typeNow consider a consumer of this moudule:
import * as Result from "./result";
export function itsOk(): Result.Result<string, string> {
const ok: Result.Result<string, string> = Result.Ok("");
const err: Result.Result<string, string> = Result.Err("");
return ok;
}There are execessive type annotations in this example but the point is that it is annoying ot have to refer to the type Result as Result.Result. I would like to refer to the type as only Result but still have functions related to the module in a Result namespace.
I was reading about the new import type syntax in 3.8 and thought I might get away with something like this:
import * as Result from "./result";
import type { Result } from "./result";
export function itsOk(): Result<string, string> {
const ok: Result<string, string> = Result.Ok("")
const err: Result<string, string> = Result.Err("")
return ok;
}This fails because there are duplicate identifiers. Typescript does allow duplicate identifiers that live either 100% in the type world or 100% in the concrete world. But imported namespace objects (import * as) lives in both worlds so it does not currently work. Specifically namespace object can contain both types and concrete things.
However I think typescript should be able to infer from usage if I'm referring the type or the namespace when using it in a place where types can be used. If what is referenced could be found by looking at the usage then duplicate identifiers could be allowed. I believe some other languages does it this way.
In this example the type used does not have a dot so it is referring to the imported type rather than the imported namespace.
const ok: Result<string, string> = ..In this example the type used has a dot so it is referring to the namespace object.
const ok: Result.OtherType = ..I think this is not related to the ECMA standard for modules and import/export but rather something typescript decide how to handle because it is fully in the "type world" which gets erased when emitted to js.
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.
Activity
jonaskello commentedon Feb 9, 2020
Note that the solution does not necessarily need to use
import *. Some other way to allow a namespace to co-exist with a type would also cover it. In simple cases like theResultexample above where the module export a single type it is actually possible to make it work today by using an intermediate file like this:index.ts
And in the consuming module you can use it like this:
Notice how the consumer can use
Resultwhen referring to the type. This is really nice, however it breaks down if the result module would export a second type as referring to other types likeResult.OtherTypewill not work (sinceResultnow is aconstand not a namespace).In this case it would be nice if we could do something like this in the intermediate file:
index.ts
Conaclos commentedon Jun 30, 2021
Some additions and a workaround :)
TypeScript already accepts some form of co-existance.
For example, the next code is valid :
However, the code cannot be tree-shaked.
Curiously, TypeScript also accept this code:
But import only the type :
To resolve this issue, we can create an indirection:
I discover this workaround several years ago. However I am still afraid of a regression in a next version of TypeScript. It could be great whether the design team could clarify the situation and provide a full support for co-existance.