Skip to content

Type parameter constraint not satisfied anymore since 5.1 #55743

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

Closed
martinhiller opened this issue Sep 14, 2023 · 7 comments
Closed

Type parameter constraint not satisfied anymore since 5.1 #55743

martinhiller opened this issue Sep 14, 2023 · 7 comments
Assignees
Labels
Needs Investigation This issue needs a team member to investigate its status. Needs More Info The issue still hasn't been fully clarified

Comments

@martinhiller
Copy link
Contributor

πŸ”Ž Search Terms

  • generic
  • type
  • constraint
  • satisfy
  • not assignable to type 'never'

πŸ•— Version & Regression Information

  • This changed between versions 5.0.4 and 5.1.6

⏯ Playground Link

https://www.typescriptlang.org/play?#code/C4TwDgpgBAMglsCAnAhgGygXigbwLABQUUA1hCAFxQBEA0gKICa1A3IcWCnEgM4XvEoAH1wDBgtBABmwKtUkyAaugCuEAIysx4qEjgBzABayaeo8GVo1mtkR0BfbSPx2dUBSfnSLqiACYtVx0zYzkQnyt-QLd7W1jCQlBIKAAFLiQAHgZGKAgAD0QAOwATHigeYD1C-QA+LCgACnhEVAwAMlE7MkoobLiASgBtak5uHmoAXVtE8GgYbyymXIKIErKKqtr6tO5Fxhrhj0npgiToACUDYz3lotLyyrhqgBpYegAxABVb1fv5mT2NTq2AaO0y2TqHRcxA8VBgH0+A2G4WOCQIxQgAGM0CgkNApCpCpjgHAAPaFKBSDICbI-NYPTbPATwr50v4LCFMuznACSAHEABLffJ3MqXcx7V4sz41Lk1BrdKjZV6wt5fV7hKi8wWffpUABupLgxVsQA

πŸ’» Code

type Literal = {
  key: "KEY";
  pairs:
    | {
        left: "leftValue1";
        right: "rightValue1";
      }
    | {
        left: "leftValue2";
        right: "rightValue2";
      };
};

type Pair<KEY extends string> = (Literal & {
  key: KEY;
})["pairs"];

type Left<KEY extends string> = Pair<KEY>["left"];

type Right<KEY extends string, LEFT extends Left<KEY>> = (Pair<KEY> & {
  left: LEFT;
})["right"];

declare function f<
  KEY extends string,
  LEFT extends Left<KEY>,
  RIGHT extends Right<KEY, LEFT>,
>(key: KEY, left: LEFT, right: RIGHT): void;

πŸ™ Actual behavior

On line 27 (second to last line), the usage of type LEFT is marked with an error:

Type 'LEFT' does not satisfy the constraint 'Left<KEY>'.
  Type 'Left<KEY>' is not assignable to type 'never'.
    Type 'string' is not assignable to type 'never'.(2344)

πŸ™‚ Expected behavior

The provided type definition should be considered correct.
Type argument LEFT is constrained by Left<KEY>, which is exactly the constraint imposed by the Right type.

Additional information about the issue

No response

@jakebailey
Copy link
Member

jakebailey commented Sep 14, 2023

I have no idea if this is intentional or not, but I checked and this bisects to #53098. (It isn't fixed by #54689 either.)

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Sep 14, 2023
@ahejlsberg
Copy link
Member

ahejlsberg commented Sep 14, 2023

Hmm, before 5.1 this was just a very complicated way of saying

function f<
    KEY extends string,
    LEFT extends "leftValue1" | "leftValue2",
    RIGHT extends "rightValue1" | "rightValue2"
>(key: KEY, left: LEFT, right: RIGHT): void;

In other words, it was as if the Pair type was declared as

type Pair<KEY extends string> = Literal["pairs"];

The KEY type variable had no effect at all (you can observe this by hovering over the types and looking at the quick info).

We now defer resolution of the Pair type because KEY might be never, in which case the entire type should become never.

@martinhiller Is there a reason the types are written in this way, which seems overly complicated? Or is the repro distilled from something more complex where Literal actually has multiple variants?

@ahejlsberg ahejlsberg added the Needs More Info The issue still hasn't been fully clarified label Sep 14, 2023
@fatcerberus
Copy link

If I didn’t know any better I would think that

(Literal & { key: KEY })["pairs"]

was an attempt to pick from a discriminated union (where key is the discriminant). Does that pattern even actually work?

@ahejlsberg
Copy link
Member

ahejlsberg commented Sep 15, 2023

Does that pattern even actually work?

It does, following #53098:

type Choices = { key: "a", value: string } | { key: "b", value: number }

type Choose<K extends string> = (Choices & { key: K })["value"];

type TA = Choose<"a">;  // string
type TB = Choose<"b">;  // number
type TC = Choose<"c">;  // never

Previously, the resolved type would be string | number no matter what.

@martinhiller
Copy link
Contributor Author

@ahejlsberg Yes, you are correct that the Literal type is only that simple for the minimal reproduction. As @fatcerberus pointed out, in our real-world scenario, Literal is a union of object literals with the same structure, and the key serves as a unique identification of this union member. In fact, in our real scenario, there are several properties in Literal which together form a composite key:

type Literal =
  | {
      keyPart1: "key-1.1";
      keyPart2: "key-1.2";
      pairs:
        | {
            left: "leftValue-1.1";
            right: "rightValue-1.1";
          }
        | {
            left: "leftValue-1.2";
            right: "rightValue-1.2";
          };
    }
  | {
      keyPart1: "key-2.1";
      keyPart2: "key-2.2";
      pairs:
        | {
            left: "leftValue-2.1";
            right: "rightValue-2.1";
          }
        | {
            left: "leftValue-2.2";
            right: "rightValue-2.2";
          };
    };

So a more complete example (which better demonstrates our intention) is the following:
https://www.typescriptlang.org/play?#code/C4TwDgpgBAMglsCAnAhgGygXgLACgpQA+UA3ngRVANYQgAKKSwAjAFxQBENIAtMwHTMOAbnKVqtBkwBM7LrT79pIsZTAo4SAM6tV44mXzjjaCADNgc0xYBq6AK4RFQ0UeOUkcAOYALS509fYDs0R2cVN3cAXz1KA1j3a38OJJCwgWVXd3FAvzlc4IcnDIjsgiisihijeMiCbikWOW4eaUFS8QbGYFlOFrbMhPVNHQSCWrKKJKtzQtCnNpcxnO88gNW5sMWO7Orsicmoac5UotalHbKC-I20hYvK3ceoCrw8UEgoBk0AHgBpACiAE1mFAIAAPRAAOwAJlooFpgJ4oV4ADRQQFA6RgyEQWHwxHIrwAPiwUAAFPBEKgMAAyUhiLpMNgY4HMSpMnrsTHSVxRACUAG0OMNtBwALqud7gaAwWb-Nk46FwhFIuAo9E8pV4lWE9UksnfJAKkGa4HSYnCpISqW4D7QABKGx+YkxoIhyoJao1rvN2vxqqJqLEMABADEACr+lVyiwm5hmrHE4O4UmYClG+OJi1QemGAjHUORvlCjgFG1vXAwiAAYzQjGgZnsUJrwDgAHsoVAzC6jG7o16g76sQPA-qUwQi1GPTr4bHgFnWUmJ1AHQBJADiAAlp7iA06goueeip8m8MTydwWW70dxesejrN2FP0ddV5ud-z2AA3dtwGGuEAA

To give you some more background: The Literal type is generated by a program, representing our data model. With the additional type aliases, we're trying to shape a TypeScript API (with awesome content assist) around our generated data model.
The Pair type serves as a lookup function to "search" for the relevant union member in Literal, while Left and Right further drill down into the "pairs" provided by each entry.

What's also interesting (back to the simple example from the initial bug report): If we replace LEFT with its upper bound Left<KEY>, the error disappears.

So instead of:

declare function f<
  KEY extends string,
  LEFT extends Left<KEY>,
  RIGHT extends Right<KEY, LEFT>,
>(key: KEY, left: LEFT, right: RIGHT): void;

We can write:

declare function f<
  KEY extends string,
  LEFT extends Left<KEY>,
  RIGHT extends Right<KEY, Left<KEY>>,
>(key: KEY, left: LEFT, right: RIGHT): void;

How can Left<KEY> satisfy the constraint imposed by Right, but LEFT (which is a subtype of Left<KEY>) violates it?
Another way how the error magically disappears is by inlining the definition of Right into the function declaration:

declare function f<
  KEY extends string,
  LEFT extends Left<KEY>,
  RIGHT extends (Pair<KEY> & {
    left: LEFT;
  })["right"],
>(key: KEY, left: LEFT, right: RIGHT): void;

I hope this is helpful.

@ahejlsberg
Copy link
Member

@martinhiller Thanks for the additional info. I'm quite certain the example didn't work as intended previously. With TS 5.0 or earlier, if you hover over f in the more complete repro, you'll see that it resolves to

function f<
    KEY1 extends string,
    KEY2 extends string,
    LEFT extends "leftValue-1.1" | "leftValue-1.2" | "leftValue-2.1" | "leftValue-2.2",
    RIGHT extends "rightValue-1.1" | "rightValue-1.2" | "rightValue-2.1" | "rightValue-2.2"
>(key1: KEY1, key2: KEY2, left: LEFT, right: RIGHT): void

In other words, the key1 and key2 type parameters have no effect whatsoever, and you can pass any of the possible left and right values you care to in any combination. That means you get no errors for call like

f("foo", "bar", "leftValue-1.1", "rightValue-2.2");

In T.S 5.1 and later we appropriately defer resolution of the types. Hovering over f you'll see

function f<
    KEY1 extends string,
    KEY2 extends string,
    LEFT extends Left<KEY1, KEY2>,
    RIGHT extends Right<KEY1, KEY2, LEFT>
>(key1: KEY1, key2: KEY2, left: LEFT, right: RIGHT): void

This means that key1 and key2 actually affect the possible values for left and right, and the invalid call above now produces an error (as I'm assuming was the intent).

That said, you're getting an error because the checker can't prove that LEFT is assignable to Left<KEY1, KEY2> because the constraint for Left<KEY1, KEY2> in a write position comes out to never (i.e. it isn't possible for the checker to find values that are safely assignable to Left<KEY1, KEY2> for any combination of those type parameters). The reason it works if you replace LEFT with its upper bound is that you're then proving that Left<KEY1, KEY2> is assignable to Left<KEY1, KEY2>, which is always true for two identical types.

Not sure I can help beyond that since I don't 100% understand what you're trying to accomplish.

@martinhiller
Copy link
Contributor Author

@ahejlsberg Thank you very much for taking the time to respond in such detail!

You are correct that while TS < 5.1 does not report an error in the function declaration, you are indeed able to pass invalid left/right value combinations. In that regard, deferring the type resolution already improved the situation.

The ultimate goal of this demo scenario is that users can only invoke function f with a valid left/right value pair given that Literal contains a union entry with key1 and key2 as composite key. And in fact, this works perfectly fine despite the reported error in the function declaration (one of the reasons why we thought the reported error is unintentional).

Content assist for left after providing a valid key1/key2 composite key:
image

Content assist for right after a valid key1/key2/left combination is passed:
image

It seems we can get around the reported error by replacing the constraint for LEFT with just string instead of Left<KEY1, KEY2> in the Right type alias, without sacrificing type safety / content assist for the invocation of f:

type Right<
  KEY1 extends string,
  KEY2 extends string,
  LEFT extends string,
> = (Pair<KEY1, KEY2> & {
  left: LEFT;
})["right"];

Given that this scenario did not fully work as intended in pre 5.1, and that "reducing" Left<KEY1, KEY2> to never by the type checker is intended, I will close this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Investigation This issue needs a team member to investigate its status. Needs More Info The issue still hasn't been fully clarified
Projects
None yet
Development

No branches or pull requests

5 participants