TypeScript: `import { default as ... } ...` doesn't work as of 4.8.3

Bug Report

🔎 Search Terms

export default, import default as, namespace, declaration.

🕗 Version & Regression Information

This changed between versions 4.8.2 and 4.8.3.

⏯ Playground Link

I can’t provide a link because both Bug Workbench and CodeSandbox still don’t have 4.8.3.

💻 Code

// @flatten-js/core/index.d.ts
declare namespace Flatten {
    class Polygon {
        constructor();
    }
}

export default Flatten;

Complete typings.

// index.ts
import { default as Flatten } from '@flatten-js/core';

const x = new Flatten.Polygon();

🙁 Actual behavior

I get the following error:

error TS2339: Property 'Polygon' does not exist on type ...

When looking at Flatten, it still has a default within, so I need to access Polygon with Flatten.default.Polygon.

🙂 Expected behavior

It gets unwrapped correctly, like in 4.8.2 and before. Right now I’m having to use the following workaround:

import { default as FlattenBad } from '@flatten-js/core';
const Flatten = FlattenBad as unknown as typeof FlattenBad.default;

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 8
  • Comments: 18 (7 by maintainers)

Commits related to this issue

Most upvoted comments

Chiming in here - while I understand that the old behavior working correctly may have been unintentional, this change in 4.8.3 led to a bunch of breaking changes in my code, which doesn’t seem right for a point release.

The { default as X } clause is useful in working around issues with types when you have an ESM nodenext app and import a RequireJS dependency with a default export whose types haven’t yet been updated for nodenext support as suggested in #48845.

For example, see the ioredis ESM import snippet (which was correct as of 4.8.2 but no longer works): https://github.com/luin/ioredis#basic-usage

This worked in 4.8.2:

import { default as Redis } from "ioredis";

const redisConn: Redis = new Redis();

but is broken in 4.8.3, needing to be rewritten as:

import { default as Redis } from "ioredis";

const redisConn: Redis.Redis = new Redis.default();

to compile. I had to make a bunch of similar changes to fastify plugin imports as well. It looks like ajv is affected as well (https://github.com/ajv-validator/ajv/issues/2047), and likely many other packages.

This seems like a pretty major non-backwards-compatible change for a point release.

but is broken in 4.8.3, needing to be rewritten as:

import { default as Redis } from "ioredis";

const redisConn: Redis.Redis = new Redis.default();

This works for me… 🤷‍♂️

import RedisModule from "ioredis";

const Redis = RedisModule.default;

@weswigham, please double check my explanation 😅

@dkulchenko yeah, I understand. We didn’t realize that the bug was being used as a workaround for broken typings. Knowing what I know now, I probably would have held the fix from 4.8.3, but the proverbial toothpaste is out of the tube now.

One correction, though—the typings that are broken were always wrong; they didn’t just need to be updated for nodenext. The problem is just more observable in nodenext. Hopefully the problem libraries will get updated quickly.

TL;DR: the library’s types are wrong.

@flatten-js/core is a CommonJS module (it ships some ESM files, but these are not exposed through any standard package.json fields). This means when you do a default import of it (or a { default as Whatever } import) in Node, you get the whole module.exports object. @flatten-js/core’s module.exports is an object with roughly the same shape as Flatten, including a member named default whose value is Flatten.

❯ node --input-type=module -e 'import Flatten from "@flatten-js/core"; console.log(Flatten === Flatten.default)'
false

❯ node --input-type=module -e 'import Flatten from "@flatten-js/core"; console.log(Flatten.box === Flatten.default.box)'
true

The way you represent this in a .d.ts file is something like this:

declare function box(): any;
declare const Flatten: {
  box: typeof box;
}

export { box };
export { Flatten as default };

But instead, as you noted, the .d.ts file included says export default Flatten. This is not the same thing. It is, admittedly, really puzzling to try to figure out what export default means when you know from the file extension (.d.ts) and package.json type (omitted entirely) that you are looking at a CommonJS module, not an ES module. But we do our best and say that it represents something like module.exports.default = Flatten. Which is something that the JS file actually has, which is great, but it’s also woefully incomplete. It’s missing every other export besides default, which is why, when you import it, TypeScript shows you that it has a default and nothing else. That’s exactly what the typings say.

Now, you may object, saying that obviously we should just resolve to the default member of the module record here. But that would be incorrect. Consider this perfectly correct JS/.d.ts pair:

// index.js
Object.defineProperty(exports, '__esModule', { value: true });
module.exports.foo = "foo";
module.exports.default = "default";

// index.d.ts
export declare const foo: "foo";
declare const _default: "default";
export default _default;

What should we say the type of lib is when you import it?

import lib from "my-wacky-lib";

By your hypothetical objection, it would look like lib should be "default". But in fact, in Node, it’s an object containing properties foo and default. This is different from what runtimes/transpilers/bundlers who care about that __esModule flag would give you, so it’s quite important we get this right and warn you that Node is going to return the whole module record.

The author of @flatten-js/core was clever to make module.exports virtually indistinguishable from module.exports.default—it means that consumers will see the same API regardless of whether their module resolver cares about __esModule. Unfortunately, that same clever duplication was not copied into the type declaration file.

The fact that { default as Flatten } worked was a bug, and was causing problems for libraries that are typed correctly: https://github.com/microsoft/TypeScript/issues/49567.

@adamburgess Sure, thanks. My tsconfig.json:

{
    "compilerOptions": {
        "allowJs": true,
        "allowSyntheticDefaultImports": true,
        "esModuleInterop": false,
        "isolatedModules": true,
        "jsx": "preserve",
        "lib": ["es6", "esnext"],
        "moduleResolution": "node",
        "resolveJsonModule": true,
        "noEmit": true,
        "target": "es6",
        "module": "esnext",
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "baseUrl": "src"
    },
    "include": ["src/**/*", "src", "index.d.ts"],
    "exclude": [
        "node_modules",
        "babel.config.js",
        "metro.config.js",
        "jest.config.js"
    ]
}

My issue is probably the same with others with export {default as XYZ} from './abcd'

When I’m trying to import the component in other files using the name given with this current version of typescript I cannot make it work because I get an error telling Cannot read properties of undefined (reading 'XYZ').

It seems like they’re trying to do a UMD module

interface InitSqlJsStatic extends Function {
    (config?: SqlJsConfig): Promise<SqlJsStatic>;
    readonly default: this;
}

declare const SqlJs: InitSqlJsStatic;
declare namespace SqlJs {
    export { InitSqlJsStatic };
}

export = SqlJs;
export as namespace SqlJs;