Skip to content

Declaration emit leaks namespace declarations for @internal functions with static property assignments #3983

@MgmClientGuy

Description

@MgmClientGuy

Summary

When a function has static property assignments (e.g. fn.prop = value), tsgo emits the merged namespace declaration into the .d.ts even when the function is marked @internal (with stripInternal: true) — the function declaration is correctly stripped, but the namespace leaks.

tsc (6.0.3) correctly omits the function in this case.

NOTE: i used copilot to write the description and verified the behavior in our codebase manually.

Steps to reproduce

input.ts:

/** @internal */
export function internalFn(): string {
    return "hello";
}
internalFn.debugFlag = true;

export function publicFn(): void {}
publicFn.metadata = "public";

tsconfig.json:

{
    "compilerOptions": {
        "declaration": true,
        "stripInternal": true,
        "module": "NodeNext",
        "moduleResolution": "NodeNext",
        "outDir": "./out",
        "target": "ES2024"
    },
    "include": ["input.ts"]
}

Behavior with typescript@6.0

Using 6.0.3, you get what you expect in the d.ts file:

export declare function publicFn(): void;
export declare namespace publicFn {
    var metadata: string;
}

// internalFn is not here, since it was stripped

Behavior with tsgo

With the @beta version, you get an unexpected result:

// the function was stripped, but "its prop" survived
export declare namespace internalFn {
    var debugFlag: boolean;
}

// this is the same as above
export declare function publicFn(): void;
export declare namespace publicFn {
    var metadata: string;
}

Maybe that's intentional now? I wasn't able to find this in the migration docs however.

Use case

In our codebase, we have this pattern with some React components for debugging purposes (e.g., some internal component MyComponent that does MyComponent.whyDidYouRender = true).

DynamicMainMenu.tsx:

import type { JSX } from "react";
import type { MainMenuProps } from "./MainMenu.js";

/** @internal */
export function DynamicMainMenu(props: MainMenuProps): JSX.Element {
	return <></>;
}

DynamicMainMenu.whyDidYouRender = true;

becomes this (note the unused imports stay in the file):

DynamicMainMenu.d.ts:

import type { JSX } from "react";
import type { MainMenuProps } from "./MainMenu.js";
export declare namespace DynamicMainMenu {
    var whyDidYouRender: boolean;
}

whereas in 6.0.3, it just became an empty export statement.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions