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

Constants + Generic causing TS4023 ("but cannot be named") #37888

Open
arcanis opened this issue Apr 10, 2020 · 8 comments
Open

Constants + Generic causing TS4023 ("but cannot be named") #37888

arcanis opened this issue Apr 10, 2020 · 8 comments
Assignees
Labels
Needs Investigation This issue needs a team member to investigate its status. Rescheduled This issue was previously scheduled to an earlier milestone

Comments

@arcanis
Copy link

arcanis commented Apr 10, 2020

TypeScript Version: 3.8, but also the 3.9 beta

Search Terms: ts4023 cannot be named constant

Code

Create a tsconfig with declaration: true. Then, in main.ts:

import {StructType} from './lib';

declare function foo<T>(): T;

export const exportedFooFunc = foo<StructType>();

In lib.ts:

export const SOME_CONSTANT = 'fieldKey';

export type MakeStruct<S> = S;
export type StructType = MakeStruct<{
    readonly [SOME_CONSTANT]: any;
}>;

Old instructions (don't repro anymore)

Create a tsconfig with declaration: true. Then, in main.ts:

import {
    ReturnType1,
    ReturnType2,
} from './lib';

export const impl1 = (): ReturnType1 => ({
});

export const impl2 = (): ReturnType2 => ({
});

In lib.ts:

export const SOME_CONSTANT = 'myConstant';

export type WrapType<S> = S;

export type ReturnType1 = WrapType<{
    [SOME_CONSTANT]?: {};
}>;

export type ReturnType2 = {
    [SOME_CONSTANT]?: {};
};

Expected behavior:

The file should be properly compiled.

Actual behavior:

TypeScript reports a diagnostic:

Exported variable 'exportedFooFunc' has or is using name 'SOME_CONSTANT' from external module "/.../lib" but cannot be named.

Playground Link: n/a

Related Issues: n/a

@arcanis
Copy link
Author

arcanis commented Apr 10, 2020

Interestingly, changing ReturnType1 into:

export type ReturnType1 = WrapType<ReturnType2>;

works, even though it should be exactly the same, except we go through one additional level of indirection. I'm also not sure why, but the generated declaration for main.d.ts then becomes:

import { ReturnType2 } from './lib';
export declare const impl1: () => import("./lib").WrapType<ReturnType2>;
export declare const impl2: () => ReturnType2;

Why did ReturnType1 get inlined whereas ReturnType2 didn't?

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Apr 13, 2020
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 4.0 milestone Apr 13, 2020
@RyanCavanaugh RyanCavanaugh added the Rescheduled This issue was previously scheduled to an earlier milestone label Aug 31, 2020
@unional
Copy link
Contributor

unional commented Sep 21, 2020

I'm able to reproduce it without generics.

Repro here: https://github.com/cyberuni/typescript-37888

// types.ts
export const typeSym = 'type'

// null.ts
import { typeSym } from './type'

export const nil = { [typeSym]: 'null' as const }

// optional.ts
import { nil } from './null'

export const optional = { nil } // 'typeSym' cannot be named

I explain it in detail here:

image

@laverdet
Copy link
Contributor

laverdet commented Mar 9, 2021

I'm running into this error while trying to emit declarations on code using unique symbols. Simple repro follows:

a.ts:

export const S = Symbol();
export const a = { [S]: null };

b.ts:

import { a } from './a';
export const b = a;
//           ^ Exported variable 'b' has or is using name 'S' from external module "./a" but cannot be named. ts(4023)

The emitter seems to be mainly failing on object keys. If instead I rewrite a.ts as:

export const S = Symbol();
export const a = { fn: (p: typeof S) => {} };

Then it works fine and the d.ts looks like this:

export declare const b: {
    fn: (p: typeof import("./a").S) => void;
};

@arcanis
Copy link
Author

arcanis commented Apr 9, 2021

Just got hit by this again while trying to use references on our project. Made a search, and found my own issue 😄

@wincent
Copy link
Contributor

wincent commented Apr 13, 2021

Ran into this with symbols just now in composite projects using project references and TypeScript v4.2.3 (haven't tried v4.2.4 which just came out yet, but based on the release notes I don't expect the behavior to be different).

In this example one project (a) exports a function create() that returns an object with a symbol (A) as key. The other project (b) attempts to call create() but doesn't explicitly reference the symbol at all; however, export-ing it is enough to prevent compilation from working, and obviously also prevents the emission of .d.ts files:

// a.ts
const A = Symbol('hey');

export function create() {
    return {[A]: true]};
}

// a.d.ts
declare const A: unique symbol; // etc...

// b.ts
import {create} from 'a.ts';

const b = create(); // TS4023 "has or is using name 'A' from external module "..." but cannot be named

export default b; // Note the error only happens if you export; otherwise the call to `create()` is fine.

In this case, I can workaround by using a string key instead of the symbol; eg.

// a.ts
const A = 'hey'; // No TS4023 any more.

austinchiang pushed a commit to austinchiang/liferay-portal that referenced this issue Apr 19, 2021
As noted in this issue:

    microsoft/TypeScript#37888

And specifically in my comment:

    microsoft/TypeScript#37888 (comment)

There is a problem in the TypeScript compiler involving indirect
references to unique Symbol types defined in projects connected by via
project references.

In short, `frontend-js-state-web` uses private `Symbol` instances as an
implementation detail, and these wind up in the type declaration files
as, eg:

    declare const ATOM: unique symbol;

So, when another project attempts to use a function like
`Liferay.State.atom()` that returns an object of a type involving that
symbol, TS may complain with:

    TS4023: Exported variable 'blah' has or is using name 'ATOM' from
    external module "path/to/State" but cannot be named.

The exact conditions required to trigger this are subtle, because using
`atom()` is working fine in places like the `frontend-js-react-web`
tests. My theory is that those work fine because the uses are all
internal, so TS doesn't need to emit any type information involving the
symbol.

But in the case of `item-selector-taglib`, which I am working on for the
purposes of this LPS, I have to export the atom, and that's when the
error pops up. Remove the `export` and there is no error. So the problem
is that TS has to emit a `.d.ts` file and it can't, because it can't
"see" the value of the symbol in the consuming module.

We can make the error go away like I am here in this commit, switching
from a symbol to a plain old string. These objects are read-only and
cannot be manipulated directly, so this is going to be fine, although I
did like the way the symbol keys were "invisible" from the outside.
austinchiang pushed a commit to austinchiang/liferay-portal that referenced this issue Apr 22, 2021
As noted in this issue:

    microsoft/TypeScript#37888

And specifically in my comment:

    microsoft/TypeScript#37888 (comment)

There is a problem in the TypeScript compiler involving indirect
references to unique Symbol types defined in projects connected by via
project references.

In short, `frontend-js-state-web` uses private `Symbol` instances as an
implementation detail, and these wind up in the type declaration files
as, eg:

    declare const ATOM: unique symbol;

So, when another project attempts to use a function like
`Liferay.State.atom()` that returns an object of a type involving that
symbol, TS may complain with:

    TS4023: Exported variable 'blah' has or is using name 'ATOM' from
    external module "path/to/State" but cannot be named.

The exact conditions required to trigger this are subtle, because using
`atom()` is working fine in places like the `frontend-js-react-web`
tests. My theory is that those work fine because the uses are all
internal, so TS doesn't need to emit any type information involving the
symbol.

But in the case of `item-selector-taglib`, which I am working on for the
purposes of this LPS, I have to export the atom, and that's when the
error pops up. Remove the `export` and there is no error. So the problem
is that TS has to emit a `.d.ts` file and it can't, because it can't
"see" the value of the symbol in the consuming module.

We can make the error go away like I am here in this commit, switching
from a symbol to a plain old string. These objects are read-only and
cannot be manipulated directly, so this is going to be fine, although I
did like the way the symbol keys were "invisible" from the outside.
brianchandotcom pushed a commit to brianchandotcom/liferay-portal that referenced this issue Apr 23, 2021
As noted in this issue:

    microsoft/TypeScript#37888

And specifically in my comment:

    microsoft/TypeScript#37888 (comment)

There is a problem in the TypeScript compiler involving indirect
references to unique Symbol types defined in projects connected by via
project references.

In short, `frontend-js-state-web` uses private `Symbol` instances as an
implementation detail, and these wind up in the type declaration files
as, eg:

    declare const ATOM: unique symbol;

So, when another project attempts to use a function like
`Liferay.State.atom()` that returns an object of a type involving that
symbol, TS may complain with:

    TS4023: Exported variable 'blah' has or is using name 'ATOM' from
    external module "path/to/State" but cannot be named.

The exact conditions required to trigger this are subtle, because using
`atom()` is working fine in places like the `frontend-js-react-web`
tests. My theory is that those work fine because the uses are all
internal, so TS doesn't need to emit any type information involving the
symbol.

But in the case of `item-selector-taglib`, which I am working on for the
purposes of this LPS, I have to export the atom, and that's when the
error pops up. Remove the `export` and there is no error. So the problem
is that TS has to emit a `.d.ts` file and it can't, because it can't
"see" the value of the symbol in the consuming module.

We can make the error go away like I am here in this commit, switching
from a symbol to a plain old string. These objects are read-only and
cannot be manipulated directly, so this is going to be fine, although I
did like the way the symbol keys were "invisible" from the outside.
@bluepichu
Copy link

Workaround to the "symbol-as-key" variant of this issue: if you give TS some non-unique symbol way of naming the type, it seems to work out ok. It's a bit clunky since you have to explicitly type the result, but at least it compiles!

Failing Pattern

// a.ts
export const A = Symbol("A");

export function create() {
	return { [A]: true };
}

// b.ts
import { create } from "./a";
export const b = create(); // TS4023: Exported variable 'b' has or is using name 'A' from external module...

Workaround Pattern

// a.ts
export const A = Symbol("A");

export interface WithAField<T> {
	[A]: T;
}

export function create(): WithAField<true> {
	return { [A]: true };
}

// b.ts
import { create } from "./a";
export const b = create(); // Emits as import("./a").WithAField<true>

@kalingaCoder
Copy link

Is there a falg/ config to disable these errors: TS4023

@blake-regalia
Copy link

blake-regalia commented Sep 24, 2023

For anyone running into this issue while dealing with exporting types that make use of unique symbol, I have successfully used the following workaround in TypeScript 5.2.2:

NOTE: the following example is for demonstration only, i did not test if it reproduces the issue (it might).

Example

datatypes.ts:

// some unique symbols the compiler is complaining about elsewhere
declare const TYPE: unique symbol;
declare const ID: unique symbol;

// perhaps a utility type being used to create special types
type Datatype<es_type, clarifier extends string> = {
    [TYPE]: es_type;
    [ID]: clarifier;
}  & es_type;

// the actual type being used elsewhere
export type Usd = Datatype<string, 'usd'>

main.ts:

import type {Usd} from './datatypes';

// where the error is being shown
export function cast(input: string): Usd { ... }

The Problem

As others have pointed out, this is happening because the system is breaking down the type of Usd from a type reference to its constituent type declaration, which drags the unique symbols along with it, seemingly attempting to carry them "across" the module boundary. No amount of exporting/importing the symbols themselves or dependent types seems to solve this issue.

The Workaround

In datatype.ts, adding a trivial bit of indirection to the exported type by making it generic solved the issue for me. I suspect this is because the system now defers breaking down its fully resolved type until later.

export type Usd<subtype extends string=string> = Datatype<subtype, 'usd'>;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Investigation This issue needs a team member to investigate its status. Rescheduled This issue was previously scheduled to an earlier milestone
Projects
None yet
Development

No branches or pull requests

10 participants