Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.4 regression] Broken relation of deferred index type over a mapped type on the source side #57188

Closed
Andarist opened this issue Jan 26, 2024 · 4 comments
Labels
Needs More Info The issue still hasn't been fully clarified

Comments

@Andarist
Copy link
Contributor

πŸ”Ž Search Terms

deferred index mapped type apparent keys

πŸ•— Version & Regression Information

⏯ Playground Link

https://www.typescriptlang.org/play?noErrorTruncation=true&ts=5.4.0-dev.20240110#code/PTAEAEDsHsFECd7XgFXgV0gYwIYBcBLaSALlDwwFMAoPATwAdLQA1HAG3UoGcAeFAHygAvKBQBtANaU60AGZiAugG5ajZgEluAOUoA3SvH5DR4lItCUAHnkqQAJt1DjI+wxYD85KqDJyO3JSq9EygAMI43Hi8AIIANKAAQiagMZY2do5JoF5pZInB6uHQALYM6Lax6bYOTjiQdCkA3s4A0qAEkKDSsgoximQx4q0WAL6gAGSgmJIwAO6QhaEakAbw0SjVmU4ASpRYyPa8AApITOt0rTIJp9Dn9FeNzdSgbR1dPfJioJFiw4oDUCtVSjVTUTq2eD+LDMGJYPDIAAy0AA5gQsLxOqBoBUxABlSA4BjcAAW0DwCSxOLwYlgBkgNOsNSyLRClDIUXgnRRoFGQiaL289W4BEIxDIAApuITiWS8GQUASiaTyQkSjxuDgUezafS8ABKERCRUylV4EHUNShGINOEI+DItFYESpeFI1Ho3j1OgJb0CMEQwzQ2Fu+B7OT8JWy1W6uyMjK1UCs9Qcijc3n8wWBByStwMhV0uOG4RCPTQAj2VSvbV4KNmyXF411uUWq2w22h8Muu3IcNehq+hr+y1s1328MAMSQJWMLs2TO2Y-dTsxkDkhnxprllLXG5QhYZAkFuU7lAjJuV29jh8FZFcawDDKDOBhoFuegrlHsPfgSaz8CwVMuUgFEq1AdgPUA1IO3tR10TAisPCA7lW3sfZ2BweBmCwDDuCcPE8HwSgAFkXxJTpKF4QUUDCcj2HsLCugXRM9gOeAjk5bkEhtOgfy7AAfaYHDPCj7AEOJqEzV4sDohi7AVWiCHoxjW1HWAbHgF88ERUVDA4AjgJRfgtkTTiQNAQTMDQuRRJSecEyyMyUWPUAnJMrIUEFV4vHvQwvN8MRb1AXz4DBUcUGgMJiCwLDbEU5S7H4H93Kcd9P2-UNnlecQ0ixFBkt+dSKC0nTIX0tMQNicQACIK2qxQBEUJClzDM8p1KKrqogp16uHUERyKCL4rkyAktDFK3yQD80Iy+0Ujc5iPJ-GruAA+qXNYw5eCc7iYN7M8j1eMgorKCpKP814hui2LKGGxixrmyYLteP8Xrel7Ohw9A0LIAV3v+5wK2QkDAXy8bFrqBpnoBnIWsnacHuQGruvRXqLKE6zROhgG7zcULsdefqYdeawvp+pMiYB0ZxCskTXHsCawftGq6osAnYdq7BODQ6r2eBnkIbEZbavsdbiY5z7ucoXnxbIarSalsWXuHNsxGgNKZp-KjLruuxSIYCbNvY7aKpRBInPE6ifycQWjaONhOB4fhdcgfXxOg3jQzgrBLZSB2uD4P63ixT4FCZ5AnCmJzASD15Vqg4F-JRqDw-gbh-jA14gdALRdDWZ3ZMYt2JooLhnq8NzLOEmz6eesh2kF0Oc9WQwNhdt3y+btY28LvWiQEf469c030dpmuvzA-q+TBNCcMw5g5EweEiC6QI8HQBhtaF0MbYc3Z9i2-2nZo3vXf73bPdgyDLZ10-9cNg-jZ24fDJSEKJIECUg60iOJNGX7BRsm4M1WOoAZJKRGs1E+ECi5EknmBH+acQH+WGO8boMgvip24KDa2GdBRT31AA6SWEiKkXAa4BsRpQAEVIWRCiW9XgRCiAwl6Q1T78HVlNdKWtoEJTPgwBIWCBA33enbE2hkL58TPKPautl-KHVAH1VQQA

πŸ’» Code

// @noErrorTruncation: true
type Values<T> = T[keyof T];
type IsNever<T> = [T] extends [never] ? true : false;
type Cast<A, B> = A extends B ? A : B;
type Compute<A extends any> = { [K in keyof A]: A[K] } & unknown;
type Invert<T extends Record<PropertyKey, PropertyKey>> = {
  [K in keyof T as T[K]]: K;
};

interface ActorLogic<in out TSnapshot, in out TEvent extends { type: string }> {
  transition: (snapshot: TSnapshot, message: TEvent) => TSnapshot;
}

type AnyActorLogic = ActorLogic<any, any>;

interface ActorRef<TSnapshot, TEvent extends { type: string }> {
  send: (event: TEvent) => void;
  getSnapshot: () => TSnapshot;
}

type AnyActorRef = ActorRef<any, any>;

type ActorRefFrom<T> = T extends ActorLogic<infer TSnapshot, infer TEvent>
  ? ActorRef<TSnapshot, TEvent>
  : never;

interface ProvidedActor {
  src: string;
  logic: AnyActorLogic;
  id?: string;
}

declare class StateMachine<
  TChildren extends Record<string, AnyActorRef | undefined>,
> {
  children: TChildren;
}

type ExtractLiteralString<T extends string | undefined> = T extends string
  ? string extends T
    ? never
    : T
  : never;

type ToConcreteChildren<TActor extends ProvidedActor> = {
  [A in TActor as ExtractLiteralString<A["id"]>]?: ActorRefFrom<A["logic"]>;
};

type ToChildren<TActor extends ProvidedActor> = string extends TActor["src"]
  ? Record<string, AnyActorRef>
  : Compute<
      ToConcreteChildren<TActor> &
        {
          include: {
            [id: string]: TActor extends any
              ? ActorRefFrom<TActor["logic"]> | undefined
              : never;
          };
          exclude: {};
        }[undefined extends TActor["id"] 
          ? "include"
          : string extends TActor["id"]
          ? "include"
          : "exclude"]
    >;

type ToProvidedActor<
  TChildrenMap extends Record<string, string>,
  TActors extends Record<Values<TChildrenMap>, AnyActorLogic>,
> = Values<{
  [K in keyof TActors & string]: {
    src: K;
    logic: TActors[K];
    id: IsNever<TChildrenMap> extends true
      ? string | undefined
      : K extends keyof Invert<TChildrenMap>
      ? Invert<TChildrenMap>[K]
      : string | undefined;
  };
}>;

declare function setup<
  TActors extends Record<Values<TChildrenMap>, AnyActorLogic>,
  TChildrenMap extends Record<string, string> = never,
>({
  actors,
}: {
  types?: {
    children?: TChildrenMap;
  };
  actors?: {
    [K in keyof TActors]: TActors[K];
  };
}): {
  createMachine: () => StateMachine<
    Cast<
      ToChildren<ToProvidedActor<TChildrenMap, TActors>>,
      Record<string, AnyActorRef | undefined>
    >
  >;
};

πŸ™ Actual behavior

It fails to be checked

πŸ™‚ Expected behavior

The used Cast should be enough to satisfy the constraint

Additional information about the issue

We can get rid off the error (but lose functionality):

  1. if we remove id from ToProvidedActor
  2. or if we remove Compute from ToChildren
  3. or if we remove any of the intersected types within this Compute
  4. or if we remove ExtractLiteralString filter~ from ToConcreteChildren

cc @weswigham (since it's a change from #56742 )

@RyanCavanaugh
Copy link
Member

Can we get a smaller repro? πŸ‘€

@rotu
Copy link

rotu commented Feb 1, 2024

Lol. That's a crazy long repro.

When trying to understand what's going on, I found that keyof Invert<Record<string,string>> is not string #57265 but I don't know if this is related to the root cause.

I also found that changing the definition of Cast to type Cast<A, B> = A extends B ? (A&B) : B; does resolve the issue (though notably in v5.4.0-dev.20240110 and NOT in v5.4.0-dev.20240201)

@Andarist
Copy link
Contributor Author

Andarist commented Feb 2, 2024

The best I can get now is this: TS playground. I doesn't minimize it by a lot though. At this point almost everything that I remove makes the error go away.

That said, I will investigate this within the internals and compare the current and old execution paths to learn more about this. That might make it possible for me to create a smaller repro. I just need to find some time for this deep dive.

@RyanCavanaugh RyanCavanaugh added the Needs More Info The issue still hasn't been fully clarified label Feb 2, 2024
@Andarist
Copy link
Contributor Author

Andarist commented Feb 5, 2024

It turns out that #56742 isn't exactly the culprit here. I think that once it landed it led to some weird situation in which it errored incorrectly (Cast should make TS happy here regardless of other things). However, since there was another problem in TS at that time - I don't think it's worth investigating further.

The additional problem was this incorrect PR of mine ( #57202 ) that was recently reverted here #57202 . We can go back to TS 5.0 (that predates the PR that was reverted) to see a different error: TS playground

What happens here is that the base constraint of Invert<TChildrenMap> can't be computed because it is a generic mapped type with a generic index type. So TS determines that this has a circular base constraint. So in the end, it can't validate that Invert<TChildrenMap>[K] (with the constraint of Invert<TChildrenMap>[string]) is assignable to string | undefined.

It's known that K has to be a string (it has a constraint of keyof TActors & string) and that values of Invert<TChildrenMap> are string. So it looked like it could be able to figure out that this has a string constraint but it turns out that it can't.

The fix for this is straightforward on my side. I can introduce a temporary infer type variable (Invert<TChildrenMap> extends infer TInverted) and use that in my checks: TS playground

it is kinda cheating here because it doesn't really fix the base constraint of the invert thing. It's still generic, right? But since infer type variables often "forget" about their original constraints I can abuse it here (I'm sorry!).

TInverted's constraint is unknown, keyof unknown is never and never is assignable to everything. So by introducing it, TS ends up checking smth along those lines:

type Target = string | undefined

type Source = Invert<TChildrenMap> extends infer TInverted ? keyof TActors & string extends keyof TInverted ? TInverted[keyof TInverted & keyof TActors & string] : string | undefined : never
type SourceConstraint = keyof TActors & string extends never ? never : string | undefined

type Result = SourceConstraint extends Target ? 1 : 0 // 1

Alternatively, I can just add & string to Invert<TChildrenMap>[K] to fix this constraint problem: TS playground. That's probably what I will go with here πŸ˜‰

Case closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs More Info The issue still hasn't been fully clarified
Projects
None yet
Development

No branches or pull requests

3 participants