microsoft / rushstack

Monorepo for tools developed by the Rush Stack community
https://rushstack.io/
Other
5.93k stars 596 forks source link

[api-extractor] Better support for nominal types when mixing alpha/beta/public .d.ts rollups #4654

Open Josmithr opened 6 months ago

Josmithr commented 6 months ago

Overview

My team is currently leveraging package.json exports combined with trimmed type rollups to manage our package APIs. We have our packages configured with something like the following:

    "exports": {
        ".": {
            "import": {
                "types": "./dist/public.d.ts",
                "default": "./dist/index.js"
            }
        },
        "./alpha": {
            "import": {
                "types": "./dist/alpha.d.ts",
                "default": "./dist/index.js"
            }
        },
        "./beta": {
            "import": {
                "types": "./dist/beta.d.ts",
                "default": "./dist/index.js"
            }
        }
    },

By default, consumers of our packages only see the public types, but we provide explicit entrypoints for our beta and alpha APIs that can be used from the same package dependency.

This is extremely convenient, as we neither need to maintain multiple packages for different release scopes, nor do customers have to change dependencies to opt into experimental features.

While this pattern works fine for structurally typed exports, it breaks down for nominally typed exports.

Example Scenario

Consider the following example, the code for which can be found here:

A repository contains 2 packages with the following dependency relationship: package-b -depends-on-> package-a.

package-a exports the following nominally typed class:

export class Foo {
    protected readonly iAmNominal?: unknown;
}

package-a is leveraging package.json exports to surface 2 variations of its API:

package-b consumes nominally typed class Foo, but it does so via a mix of the two export paths (this is a contrived example, but it reflects a scenario that occurs easily with more code modules). package-b has the following code:

import { Foo as Foo_A } from 'package-a';
import { Foo as Foo_B } from 'package-a/internal';

/**
 * @public
 */
export const foo: Foo_A = new Foo_B();

Ideally, this would work fine - both of the rollups being leveraged refer to the same underlying type. Unfortunately, since API-Extractor generates the rollups via redeclarations, TypeScript deems the 2 nominally typed class declarations as being incompatible. And, in fact, the above code in package-b fails to build.

Feature Request

Ideally, API-Extractor could surface nominally typed exports in such a way that guarantees compatibility.

A naive solution, which we have adopted internally, is to simply re-export existing declarations from the d.ts entrypoint API-Extractor consumes, rather than redeclaring them. It isn't immediately clear to me if such a pattern is compatible with API-Extractor's use-cases or not.

Would love to hear thoughts from the maintainers on this.  

octogonz commented 4 days ago

We discussed this topic. I think it breaks down into some subproblems: