fastify / fastify-plugin

Plugin helper for Fastify
MIT License
197 stars 42 forks source link

types: Export types in a way compatible with TypeScript `"moduleResolution"`: `"NodeNext"` #200

Closed brettwillis closed 1 year ago

brettwillis commented 1 year ago

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

mcollina commented 1 year ago

@fastify/typescript could you take a look?

fox1t commented 1 year ago

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.

climba03003 commented 1 year ago

@fox1t You may want to take a look at https://github.com/fastify/fastify-cookie/pull/184 which is the similar approach.

fox1t commented 1 year ago

@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?

climba03003 commented 1 year ago

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.

fox1t commented 1 year ago

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

fox1t commented 1 year ago

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?

Uzlopak commented 1 year ago

Can we have a test for the last change?

brettwillis commented 1 year ago

Sorry for the delay, will put together the repro tomorrow.

brettwillis commented 1 year ago

Hi all, please see this branch in my fork https://github.com/brettwillis/fastify-plugin/tree/fix-typings-repros, and find the directories named:

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

Please double-check my work and let me know your thoughts.

mcollina commented 1 year ago

@climba03003 @fastify/typescript I completely defer to you on this one.

brettwillis commented 1 year ago

@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.