Repository navigation
ESM X.test which have zero export statement, should be sub type of ESM X which have at least one export statement #57735
Description
Activity
- changed the title
[-]dynamic import `foo.test.ts` without `export default {}` leads to wrong type union[/-][+]merge dynamic import `foo.test.ts` without `export default {}` leads to wrong type union[/+]on Mar 12, 2024 - addedNot a DefectThis behavior is one of several equally-correct optionsThis behavior is one of several equally-correct options
on Mar 12, 2024 RyanCavanaugh commented
on Mar 12, 2024 MemberMore actionsYou have two modules shapes here:
- The one from
a, which has afoo: numberproperty - The one from
a.test, which has either nothing, nothing, or adefault: {}property
The "nothing" variants supertype the
{ foo: number }property, so the unionimport(...) | import(...)gets reduced to the more-general of the two (nothing)- The one from
Ryan Cavanaugh (@RyanCavanaugh)
But if
a.testdon't have export statement, I think it should be something likeRecord<string, never>.Because the only way to let ESM module have
Xproperty, is usingexportstatement.If there is zero export statement, it means a zero key object
Record<string, never>.Right?
Change to
a.test.mts-
still get
typeof import("/path/to/reproduce/a.test") -
instead of something like
{foo: 42} | Record<string, never>.
-
RyanCavanaugh commented
on Mar 13, 2024 MemberMore actionsThat would be a very bad type; given
x: Record<string, never>you can write nonsense likeMath.sin(x.foo)without errorQuote recommendation from
@typescript-eslint/ban-typesIf you want a type meaning "empty object", you probably want
Record<string, never>instead.The TypeScript maintainers have a... let's say... contentious relationship with the
ban-typesrule, specifically the part about it banning{}Either way,
Record<string, never>doesn't mean "empty object", regardless of what eslint recommends. It actually means "object where accessing any property throws an exception". Reading a property from an empty object doesn't give you a value of typenever(i.e. an exception), it gives youundefined.But, compared with
{}which means any-alike,Record<string, never>seems to be yet the most correct type, to describe empty-object, maybe say always-zero-key-object, which describe an ESM module without any export statement.RyanCavanaugh commented
on Mar 15, 2024 MemberMore actionsIf you want a type meaning "empty object", you probably want Record<string, never> instead.
The typescript-eslint maintainers are uncharacteristically super wrong about this.
{ }is a valid type with a valid meaning and it's just wrong for them to ban it. Unfortunately we haven't had much luck changing their minds.JoshuaKGoldberg commented
on Mar 17, 2024 ContributorMore actions👋 typescript-eslint maintainer here. The
ban-typesrule's default options were formed many years ago when TypeScript had less fleshed out rules around{},object, and related. If there's evidence that the rule's options are now and/or always were wrong about something, we'd happily take an issue to improve them. The dev lead for TypeScript saying the rule is super wrong is pretty compelling evidence. 😄Also looking at typescript-eslint/typescript-eslint#5018 (comment):
There's just a lot of confusion likely to come down the road on this specific ban in general, since we're likely to change the definition of NonNullable to T & { } in 4.8, and are already in 4.8 going to change the default narrowing rules such that a truthy narrowing of a value of an unconstrained type parameter becomes T & { }. Having typescript-eslint, out of the box, ban you from writing a type that TypeScript itself is inferring under totally normal operation, is awkward.
TypeScript did in fact ship the change to make
type NonNullable<T> = T & {};in #49119 - along with a host of other{}improvements. I'd wager that there's even more evidence we should revisit the ban.Three related issues on our side:
- Bug: [ban-types] Should suggest object, not Record<string, never> typescript-eslint/typescript-eslint#5947: the most recent example I know of around making the rule move away from
Record<string, never> - Enhancement: [ban-types] Allow {} in the recommended config typescript-eslint/typescript-eslint#8697: sparked by Ryan generally referencing
ban-typesand{}vs.Record<string, never> - Enhancement: [ban-types] Split the {} ban into a separate, better-phrased rule typescript-eslint/typescript-eslint#8700: sparked by this discussion too
For context: typescript-eslint is a separate maintenance team from TypeScript. Our opinions can sometimes fall out of sync with TypeScript best practices because we -like many independent open source projects- generally only take action in response to user prodding. We haven't been prodded about
ban-types(that I know of) in quite a while. If there's anythingban-typesor any other rule is doing that's out of sync with how TypeScript is meant to be, and TypeScript's handling of that rule's area has changed since the last time that rule was discussed, we'd happily take an issue prodding us to change. ❤️- Bug: [ban-types] Should suggest object, not Record<string, never> typescript-eslint/typescript-eslint#5947: the most recent example I know of around making the rule move away from
As far as I see, maybe should split
empty object typeinto another issue, if needed, as there are now two issue?Josh Ghoulberg 👻 (@JoshuaKGoldberg)
The original issue is
Actual
ESM
X.testwhich have zero export statement, is actually super type of ESMXwhich have at least one export statementExpected
ESM
X.testwhich have zero export statement, should be sub type of ESMXwhich have at least one export statement.Because it is
-
zero-key-object-at-read -
not
zero-key-object-at-write, but consider as nop
> mod = await import('/tmp/empty-file.mjs') > mod.x=1 1 > mod.x undefined
-
- changed the title
[-]merge dynamic import `foo.test.ts` without `export default {}` leads to wrong type union[/-][+]ESM `X.test` which have zero export statement, should be sub type of ESM `X` which have at least one export statement[/+]on Mar 17, 2024 RyanCavanaugh commented
on Mar 17, 2024 MemberMore actionsThere's no type in TypeScript that means "a type that is guaranteed to be completely empty" because TypeScript doesn't have sealed/exact types
typescript-bot commented
on Mar 20, 2024 ContributorMore actionsThis issue has been marked as "Not a Defect" and has seen no recent activity. It has been automatically closed for house-keeping purposes.
- locked as resolved and limited conversation to collaborators
on Oct 22, 2025
🔎 Search Terms
dynamic import
type union
no
exportexport {}export default {}🕗 Version & Regression Information
⏯ Playground Link
No response
💻 Code
reproduce.ts
a.ts
🙁 Actual behavior
a.test.tstype is
a.test.tstype is
a.test.tstype is
🙂 Expected behavior
When
a.test.tstype is
Additional information about the issue
No response