Skip to content

Unconstrained generics are incorrectly assignable to _any_ "Partial" type #30634

Description

TypeScript Version: 3.4.0-dev.201xxxxx

Code

declare let x: Partial<HTMLElement>;

function f<T>(y: T) {
    x = y;
}

f({ innerHtml: Symbol() }); // this is blatantly incompatible with `HTMLElement`

if (x.innerHTML) {
    x.innerHTML.toLowerCase(); // We just set it to a `symbol` and not a `string`, this will error at runtime
}

Expected behavior:
An error on x = y.

Actual behavior:
No error.

Playground Link

The root cause is this relationship in structuredTypeRelatedTo added way back in this:

if (relation !== subtypeRelation && isPartialMappedType(target) && isEmptyObjectType(source)) {
    return Ternary.True;
}

This is unsound when source is the empty type resulting from the constraint of an unconstrained generic type (which, if changed to unknown, catches this issue!). It's unsound when it comes from a constraint at all, actually. Fixing this will break react-redux, whose recursive Shared type (which we have in our test suite) actually only checks because of this unsoundness.

Activity

  1. RyanCavanaugh commented on Mar 28, 2019

    @RyanCavanaugh
    Member

    Not a regression

  2. weswigham commented on Mar 28, 2019

    @weswigham
    MemberAuthor

    As I said in the OP, the change that introduced the bug was #26517 from what I can tell, so we've had it long enough that we have people unintentionally relying on it. react-redux's very terrible no good Shared type:

    export type Shared<
        InjectedProps,
        DecorationTargetProps extends Shared<InjectedProps, DecorationTargetProps>
        > = {
            [P in Extract<keyof InjectedProps, keyof DecorationTargetProps>]?: InjectedProps[P] extends DecorationTargetProps[P] ? DecorationTargetProps[P] : never;
        };

    only actually typechecks in most cases because Shared is partial and most uses involve unconstrained type parameters. "no constraint" is defaulted to the empty object, which is then assignable to any partial type... like Shared. This is the only thing allowing most uses of this highly suspect pattern to pass the typechecker. Specifically this one is core - GetProps<C> is unconstrained - we know absolutely nothing about it (it's an infer type parameter or never), so how could is possibly satisfy the Shared<InjectedProps, DecorationTargetProps> constraint? It shouldn't be able to, yet this bug is why it does~

  3. jakebailey commented on Jul 18, 2023

    @jakebailey
    Member

    This one was fixed in #49119, explicitly via the first bullet in the PR description:

    An unconstrained type parameter is no longer assignable to {}.

    But, could probably use an explicit test.

  4. locked as resolved and limited conversation to collaborators on Jan 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugA bug in TypeScript

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions