Skip to content

Exported const type parameters is not renamed in constraint in another const in type declarations #56783

Description

🔎 Search Terms

Generic methods
Constraints
Type parameters renaming
Type declarations

🕗 Version & Regression Information

⏯ Playground Link

https://www.typescriptlang.org/play?target=7&allowSyntheticDefaultImports=false&ts=5.3.2#code/C4TwDgpgBACgTAHgDIBooCUB8UC8UlQBkUA3lANoDSUAlgHZQDWEIA9gGYYC6AXFHQFcAtgCMIAJygBfANwBYAFCLQkWAgAq2PGSq0GzNp3W9+wsZKmLFAY1Z0AzsCgAxVq1ylFUb1HYIAklAQAB7AEHQAJvawiCRSaP6YmAAUAJSk8V4+AOYBQaHhUWqJKelxKIqyVgq2Dk4AQgCGktpZ3n6BIWGR0fAI5VAlaRkVCj5QuZ0FPcVJw+WV8koKIWCs4k61jlBNAF4eJG2+eV2FvbHxg3NlmWM5J9NFMAHXI4vVq+ubdtsAIhCcVp3dpuPiuVijcYiZp8JriSE+aG7WGNXajKrLIA

💻 Code

type P2<L, R> = L & { [K in keyof R]: number };

type P<T> = { [K in keyof T]: number }

const Foo = {
    f<I extends P2<{}, I>>() {},
    g<I extends P<I>>() {},
};

const Bar = {
    f<I extends P2<{}, I>>() {},
    g<I extends P<I>>() {},
};

export const Baz = {
    f<I extends P2<{}, I>>() {},
    g<I extends P<I>>() {},
};

export const Def = {
    foo: Foo,
    bar: Bar,
    baz: Baz,
};

🙁 Actual behavior

Generated .d.ts on v5.3.2 looks like this:

type P<T> = {
    [K in keyof T]: number;
};
export declare const Baz: {
    f<I extends { [K in keyof I]: number; }>(): void;
    g<I_1 extends P<I_1>>(): void;
};
export declare const Def: {
    foo: {
        f<I extends { [K in keyof I]: number; }>(): void;
        g<I_1 extends P<I_1>>(): void;
    };
    bar: {
        f<I_2 extends { [K_1 in keyof I_2]: number; }>(): void;
        g<I_3 extends P<I_3>>(): void;
    };
    baz: {
        f<I_4 extends { [K in keyof I]: number; }>(): void;
        g<I_5 extends P<I_5>>(): void;
    };
};
export {};

Notice the line f<I_4 extends { [K in keyof I]: number; }>(): void; in baz, there's no I in scope.
Same line in bar, that's f<I_2 extends { [K_1 in keyof I_2]: number; }>(): void; has type parameters correctly renamed, which points to exported consts specifically.
g<I_5 extends P<I_5>>(): void; also have correct type params, so simple generic type like P is not enough.

🙂 Expected behavior

Generated .d.ts on v4.3.5 looks like this:

declare type P<T> = {
    [K in keyof T]: number;
};
export declare const Baz: {
    f<I extends { [K in keyof I]: number; }>(): void;
    g<I_1 extends P<I_1>>(): void;
};
export declare const Def: {
    foo: {
        f<I extends { [K in keyof I]: number; }>(): void;
        g<I_1 extends P<I_1>>(): void;
    };
    bar: {
        f<I_2 extends { [K_1 in keyof I_2]: number; }>(): void;
        g<I_3 extends P<I_3>>(): void;
    };
    baz: {
        f<I_4 extends { [K_2 in keyof I_4]: number; }>(): void;
        g<I_5 extends P<I_5>>(): void;
    };
};
export {};

Notice the line f<I_4 extends { [K_2 in keyof I_4]: number; }>(): void;, all type parameters nave been correctly renamed.

Additional information about the issue

Bisected commit passes sniff test: it introduces type caching and accessibility tracking, so maybe some of that triggers when emitting first constant, and poisons cache for visitDeclarationSubtree -> ... -> signatureToSignatureDeclarationHelper -> ... -> typeParameterToDeclaration call later

Origin of this issue is code generated by ts-proto with outputServices=generic-definitions option. See gist for example. Problem occuring here, on lines 70-79 in a.d.ts, where create and fromPartial have renamed type parameter, but original constraint (seemingly, from export declare const FooResponse)

Activity

  1. fatcerberus commented on Dec 14, 2023

    @fatcerberus

    I_4 extends { [K_2 in keyof I_4]: number; }

    I’m not sure how to interpret this constraint - the definition seems circular.

  2. RyanCavanaugh commented on Dec 14, 2023

    @RyanCavanaugh
    Member

    Have you checked this in nightly? We recently had a fix in this area that may have already addressed it

  3. mcheshkov commented on Dec 15, 2023

    @mcheshkov
    Author

    Nightly on playground (v5.4.0-dev.20231215) generates broken typings as well: f<I_4 extends { [K in keyof I]: number; }>(): void;

  4. mcheshkov commented on Dec 15, 2023

    @mcheshkov
    Author

    Bruce Pascoe (@fatcerberus) it can read as "number should be assignable to every property of I_4"

    In ts-proto similar construction is used for exact types. Rougly like this: f<I extends ConcreteType & { [K in Exclude<keyof I, keyof ConcreteType>]: never; }>(i: I), meaning that every property in I, that is not a propery of ConcreteType should be never, rendering type with any extra property never.

  5. RyanCavanaugh commented on Sep 19, 2026

    @RyanCavanaugh
    Member

    This is fixed in TypeScript 5.5.0-dev.20240517 and later. The declaration emitter now preserves P2<{}, I> for Def.baz.f instead of expanding its constraint with an out-of-scope I reference. The change is in #58539.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugA bug in TypeScriptDomain: Declaration EmitThe issue relates to the emission of d.ts filesFixedA PR has been merged for this issue

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions