fastify-plugin
fastify-plugin copied to clipboard
types: Export types in a way compatible with TypeScript `"moduleResolution"`: `"NodeNext"`
This is a typing issue only, not a runtime isse. Since TypeScript 4.8.3
, when using "moduleResolution": "NodeNext"
(or "Node16"
) for an ESM project, TypeScript will now report an error when using the code below:
import fp from 'fastify-plugin';
export default fp(...); // Error: This expression is not callable. Type 'typeof import(".../node_modules/fastify-plugin/plugin")' has no call signatures.
My own understanding is limited, but there has been a lot of discussion, for example microsoft/TypeScript#48845. Basically I think it is because the typings descibe the module as an ES module with a default export (which would be correct if the module was an ES module), but it is actually a CJS module exporing the plugin
function with a default
property...
Either way, I've tested this PR in an ESM project with "moduleResolution"
: "Node"
and "moduleResolution"
: "NodeNext"
(or "Node16"
) and it compiles successfully for both.
Checklist
- [x] run
npm run test
andnpm run benchmark
- [x] ~tests and/or benchmarks are included~
- [x] ~documentation is changed or added~
- [x] commit message and code follows the Developer's Certification of Origin and the Code of conduct
@fastify/typescript could you take a look?
Oh. This could be a problem, and we should find a solution. @brettwillis, we export default (and map it in JS code) to support every environment, bundler, and compiler out there. We call it the "famous triplet" because it allows us to make Fastify modules compatible with ESM, faux ESM, TS, and CJS. Changing our types to "namespace" will make them usable only with "import * as something from 'something'
under TS (or IDE that infers types for js files, such as VS Code) if "moduleInterop" is not set to true. Can you please make a repro repository I can take a look at? I want to understand if there are less radical solutions here.
@fox1t You may want to take a look at https://github.com/fastify/fastify-cookie/pull/184 which is the similar approach.
@climba03003 I am starting to think that the era of the "famous triplet" is over. If so, we should update all of our types accordingly. As far as I understood from your link the issue is "NodeNext"
+ "type": "module"
, right?
I am starting to think that the era of the "famous triplet" is over.
Yes. and No. It still supported somehow, but it has more problem pop-up. Including https://github.com/fastify/fastify-cors/pull/231#issuecomment-1278564075
If so, we should update all of our types accordingly.
Yes
As far as I understood from your link the issue is
"NodeNext"
+"type": "module"
, right?
Yes, this combination is the major problem.
After further discussion with @climba03003 and basic repro on my machine, I would not like to make these types CJS (aka using export =
syntax with the namespace). We should add explicit famous triplet
to fp
, instead, as suggested here https://github.com/fastify/fastify-cors/pull/231#issuecomment-1280604005
P.S. I still want to see a repro repo here because I am unsure about the fix. @brettwillis would you mind providing it, please?
Can we have a test for the last change?
Sorry for the delay, will put together the repro tomorrow.
Hi all, please see this branch in my fork https://github.com/brettwillis/fastify-plugin/tree/fix-typings-repros, and find the directories named:
- types-repro-commonjs-new (this PR with CommonJS setup)
- types-repro-commonjs-old (current typings with CommonJS setup)
- types-repro-esm-nodenext-new (this PR with ESM/NodeNext setup)
- types-repro-esm-nodenext-old (current typings with ESM/NodeNext setup)
The test setup looks like this
import fp1, { PluginOptions } from 'fastify-plugin';
import { default as fp2, PluginMetadata } from 'fastify-plugin';
import * as fp3 from 'fastify-plugin';
console.log(typeof fp1); // function
console.log(typeof fp1.default); // function
console.log(typeof fp2); // function
console.log(typeof fp2.default); // function
console.log(typeof fp3); // object
console.log(typeof fp3.default); // function
fp1(async () => {});
fp2(async () => {});
// esModuleInterop has no influence here, always type error and runtime error, so the typing is correct, this is consistent with old
// @ts-expect-error
fp3(async () => {}); // runtime TypeError: fp3 is not a function
In summary
- The
import fp from 'fastify-plugin'
syntax always works at runtime (regardless of"esModuleInterop"
)- the current types cause an incorrect type error with
"moduleResolution": "NodeNext"
(incorrect because it works at runtime) - the new types fix the error
- the new types also correctly identify the presence of the
default
property on the exported function which exists at runtime (but is missing in the current types)
- the current types cause an incorrect type error with
- The
import * as fp from 'fastify-plugin'
syntax only works with"esModuleInterop": false
and is correctly represented by both the old and new types (with"esModuleInterop": true
we must usefp.default
)
Please double-check my work and let me know your thoughts.
@climba03003 @fastify/typescript I completely defer to you on this one.
@climba03003 apologies for the delay, it's pretty busy on my end. I've added tests for the the different import styles, but I'm unsure how to go about invoking with different tsconfig.json
? So I believe for now tsd
is just using "module": "commonjs", "moduleResolution": "node"
. Will try to figure something out in the coming days.