Skip to content

Index type distributes over intersections too eagerlyΒ #61101

Open
@Andarist

Description

@Andarist

πŸ”Ž Search Terms

index keyof indexed access deferred deferral instantiation

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.8.0-dev.20250202#code/C4TwDgpgBAaghgGwK4QM4B4AqA+KBeKTAbQGsIQB7AM0IF0BuAKEYHoWpgAnOAO1SoqcAtqihVOFIVABEZEBAAmUCgCMAVhADGwKELhhpHClDhQkPAJYUejUJCgBRAB5c42hwDcIPYKgBiEkIACnAgCBRwCgCy+li4BPDIaOgA3oxQUEQA0lAWPFBy1HQAXITZtFAAZFApHOAQpTkAvkxN2Ey29Y48AI4oKADy6lrAWA5CFsDAip7eOhAu3gqitXYNUEESkJygWeRQbfg16VBsUJqcEHDTomASTiC6EMAAFhTLJxATwKVpGRnZXL5TDjSbTBSzHxEaRraS0WilAAUJ3+UDAoXCkVKA2+Y2+4MhOmqq3qjQOABoZLDsOSUVAAJT4XAeCgWBRMDItRhczr2ZxaPFgxRQBbTHjLGp1SClTYUba7faHAjIjLeHqlBy9foQIYabSCqaKGmMRl4Zms9nMBRaBBwS5iczaKz5C5XaYAZWAgggBvB2ER1qoeUmzt+n2+qDDqMBeQK5CKIPxigRUER6LCEQUpUTQoU5VN5rZHIOxYWWg1TgFzlc7i8Pn8gRCGciMTAvqN7W59NKvBAHVd1wgnu9iL+IojUf+eQHqEUSIA+r8zGAAEIgUo8JBCFQQTgHAs1Jq01HWmdz1OLyUKCgAdx4a43W53e6aB5SR5OH9Vlc0SLVb7pNUADovkmIDp0uOBZwUUc6QyJBV3XGQVDgAAvPIAHM4GkSkzleCxRFQN4kAQJRdwkTgAEI6VfDpUU0axUB0Kh8gIYDQOAcCeDPS1UTOf4AD0AH5mD49h8NEAioDUJAmJMc45UeIobxea5clEBQCLAW15CUKCoAAA3Y74uJ4gyAHJRDWOkGL4eYnDcYB3TgIQIEweoAEFUA8lQKC8JE4LRDFM2xXFAoyWoEIfKBN23XcDiqSU1lKaQIKuaDDCaRKSWlGQ0qgxRMuPVF-hheppEC41UQPFk2SORE3x5VEFkc5zXPcyAvJ8vyIFgkrl2i6QUPQngsJw05xJeKTrGgcjBFEBjOEubQEBAGj6WLD9aMYIA

πŸ’» Code

type Values<T> = T[keyof T];

// transforms from "keyed object map" to a union
type ExtractEventsFromPayloadMap<T> = Values<{
  [K in keyof T]: T[K] & { type: K };
}>;

type EnqueueObject<TEmittedEvent extends { type: PropertyKey }> = {
  // creates proxy methods
  emit: {
    [K in TEmittedEvent["type"]]: (
      payload: Omit<TEmittedEvent & { type: K }, "type">,
    ) => void;
  };
};

type Exec<TEmitted extends { type: PropertyKey }> = (
  enq: EnqueueObject<TEmitted>,
) => void;

declare function createStore<TEmitted>(definition: {
  emits: {
    [K in keyof TEmitted]: (payload: TEmitted[K]) => void;
  };
  exec: Exec<ExtractEventsFromPayloadMap<TEmitted>>;
}): any;

createStore({
  emits: {
    increased: (_: { upBy: number }) => {},
    decreased: (_: { downBy: number }) => {},
  },
  exec: (enq) => {
    enq.emit.increased({
      upBy: "bazinga", // this should error!
    });

    const fn = enq.emit.increased;
    //    ^?

    // this is just a copy of what is displayed as `enq.emit.increased`'s type
    const exactSameTypeAsAbove: (
      payload: Omit<
        { upBy: number } & { type: "increased" } & { type: "increased" },
        "type"
      >,
    ) => void = () => {};

    exactSameTypeAsAbove({
      upBy: "bazinga", // this one errors correctly
    });
  },
});

πŸ™ Actual behavior

There is no error on the annotated call

πŸ™‚ Expected behavior

It should error

Additional information about the issue

  1. keyof (T & { type: K }) gets normalized to keyof T & "type"
  2. that passed through Exclude<X, "type"> (within Omit) is left as Exclude<keyof T, "type">.
  3. then T gets instantiated with ExtractEventsFromPayloadMap<{ increased: { upBy: number; }; decreased: { downBy: number; }; }
  4. but now keyof ... returns only shared keys of this union
  5. the only shared key is type so this Exclude gets computed as never (Exclude<"type", "type">)
  6. and the final parameter type of enq.emit.increased gets computed as just {} (from { [K in never]: ... })

Metadata

Metadata

Assignees

No one assigned

    Labels

    Help WantedYou can do thisPossible ImprovementThe current behavior isn't wrong, but it's possible to see that it might be better in some cases

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions