Skip to content

local types are considered exported in .d.ts files unlike local types in .ts files #57764

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

Closed
eps1lon opened this issue Mar 13, 2024 · 11 comments
Closed
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@eps1lon
Copy link
Contributor

eps1lon commented Mar 13, 2024

πŸ”Ž Search Terms

ts2460 export type

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried (down to 2.0), and I reviewed the FAQ for entries about ?

⏯ Playground Link

No response

πŸ’» Code

Full repro: https://github.com/eps1lon/ts-module-auto-export

index.tsx

import type { Private as DeclarationPrivate } from "./declaration";
import type { Private as LibraryPrivate } from "./library";

export const foo: DeclarationPrivate = { foo: "bar" };
export const bar: LibraryPrivate = { foo: "bar" };

declaration.d.ts

/**
 * @internal
 */
type Private = {
  foo: string
}

export type Public = Private | null

library.ts

/**
 * @internal
 */
type Private = {
  foo: string
}

export type Public = Private | null

πŸ™ Actual behavior

Import of local types is allowed in .d.ts files. Only Module '"./library"' declares 'Private' locally, but it is not exported.ts(2459) is raised

πŸ™‚ Expected behavior

Local types are not exposed from .d.ts files unless they have an export modifier. We should get Module '"./declaration"' declares 'Private' locally, but it is not exported.ts(2459) as well.

The other issue is that TypeScript removes the local types from the declaration for library.ts instead of keeping it as a local type.

This is a problem for libraries since every type is automatically part of the public API and we can't easily remove implementation details.

If TypeScript would hide local types, it would also need to make sure that emitted declarations don't reference those types.

The real-world issue is with React types. If TypeScript has to infer the type it may decide to either reference React.ReactNode explicitly (good) or inline all its members (bad) e.g.

class Component extends React.Component<{ children?: React.ReactNode }> {
  render() {
    if (Math.random()) {
      return <div />
    }
    return this.props.children
  }
}

will be emitted as

export declare class Component extends React.Component<{ children?: React.ReactNode }> {
    render(): string | number | boolean | Iterable<React.ReactNode> | import("react/jsx-runtime").JSX.Element | null | undefined;
}

Additional information about the issue

Original issue: vercel/next.js#63185

Maybe related to

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Mar 13, 2024
@RyanCavanaugh
Copy link
Member

This is the intended behavior unless you have an export { }; in the file (which tsc automatically inserts)

@fatcerberus
Copy link

declaration.d.ts already has an export though?

@eps1lon
Copy link
Contributor Author

eps1lon commented Mar 13, 2024

This is the intended behavior unless you have an export { }; in the file (which tsc automatically inserts)

This doesn't work in module augmentation.

// react.d.ts
export = React;
export as namespace React;
export {}

declare namespace React { }
// augmentation.d.t.s
export {}

declare module '.' {
   // can't put `export {}` here
   interface Whatever {}
}

will cause emissions of React.Whatever

Also:
it is intended behavior that it removes local types? What other bug would including local types cause?

@RyanCavanaugh
Copy link
Member

This doesn't work in module augmentation.

In module augmentation, you can already refer to types in the outer block

@typescript-bot
Copy link
Collaborator

This issue has been marked as "Working as Intended" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Mar 18, 2024
@eps1lon
Copy link
Contributor Author

eps1lon commented Mar 19, 2024

In module augmentation, you can already refer to types in the outer block

@RyanCavanaugh I don't understand what to make of this statement? I understand, that I can do that. How is that relevant here?

Edit:

You mean I should move it outside the module augmentation block and then it becomes hidden? Do we have lint rules for that in DT?

@RyanCavanaugh
Copy link
Member

You mean I should move it outside the module augmentation block and then it becomes hidden?

Correct

Do we have lint rules for that in DT?

I'm not sure how/why we would? Both locations are valid depending on intent.

@jakebailey
Copy link
Member

jakebailey commented Mar 19, 2024

If you mean to flag unused ones that aren't exposed, those will get flagged once I have time to work on typescript-eslint/typescript-eslint#8611 and add it to DT (once I reimplement these scoping rules there), but yeah, there's no way to say "are you sure you meant to export that".

@eps1lon
Copy link
Contributor Author

eps1lon commented Mar 19, 2024

but yeah, there's no way to say "are you sure you meant to export that".

I'm pretty sure if you write type A = number; export type B = string; you either didn't meant to export A, or we can just force you to write export type A = number which is what we (used to?) do in DT packages where you either have to write export type ... everywhere or place an explicit export {}.

Why we need treat .d.ts, .ts and declare module entirely separately, I do not understand. I'd get it if this is just a just-so thing for backwards compat. But you're currently arguing for this like you would do it again if you could start from scratch and that I'm just not seeing.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Mar 19, 2024

Unless explicitly asked, I'm not here to discuss what we'd do differently from a blank slate perspective. Those responses are too long to fit in these tiny textboxes.

@jakebailey
Copy link
Member

jakebailey commented Mar 19, 2024

But you're currently arguing for this like you would do it again if you could start from scratch and that I'm just not seeing.

"Intended" can mean "this sucks but we can't change it and we are intentionally not changing it".

But on DT, we can certainly try and create a lint rule that complains about mixed-exported types in modules without export {} (or force export {} in all files containing exports), or something, if that's the concern.

ptomato added a commit to js-temporal/temporal-polyfill that referenced this issue Mar 5, 2025
I learned today that TypeScript treats everything in a .d.ts file as if it
was exported, whether it says `export` or not, unless you add an
`export {}` declaration somewhere in the file. See
microsoft/TypeScript#57764

We had a bunch of ts-prune-ignore-next declarations in internaltypes.d.ts
to work around this. Adding `export {}` allows ts-prune to work as
expected without workarounds.
ptomato added a commit to js-temporal/temporal-polyfill that referenced this issue Mar 5, 2025
I learned today that TypeScript treats everything in a .d.ts file as if it
was exported, whether it says `export` or not, unless you add an
`export {}` declaration somewhere in the file. See
microsoft/TypeScript#57764

We had a bunch of ts-prune-ignore-next declarations in internaltypes.d.ts
to work around this. Adding `export {}` allows ts-prune to work as
expected without workarounds.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

5 participants