TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

`T` satisfies a constraint that `E extends T` does not (specifically with `Omit`)

Open vi013t opened this issue 5 months ago β€’ 4 comments

πŸ”Ž Search Terms

extends does not satisfy the constraint in Omit

πŸ•— Version & Regression Information

This is the behavior in every version I tried, and I reviewed the FAQ for entries about generic type parameter constraints.

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/C4TwDgpgBAKhDOwCMUC8UDeUCGAuKAdgK4C2ARhAE4A0UZ+x5VUAvgNwBQHokUACpQD2ZADYQSAHgBC2eBFoBpCCCgQAHsAgEAJvCgBrZYIBmUGXIB8aKAHkSAS2DTZ8qEpAXO3cNACialCgg4JDQsPQBYTFJSNFxCThEJFoAImwUi1SyDKgAelyoADlBKF9KIUpvXn8AJgSEYBrVDS1dWAakKwihOJie6PrEGtT0zKgU7Kt80vLBSumwxaXlldXFgD1NmYqoQQBjPaJKPQALKgggA

πŸ’» Code

Creating a nested type with a generic type parameter that extends some type can, in some cases, throw an error, whereas using the type directly does not, in some ways that feels like unexpected behavior.

The following shows two examples, one without a generic type parameter which does not error, and one with a generic type parameter which does error:

type Test1 = { a: number, b: number };

type Problem<Base, Key extends keyof Base> = Omit<Base, Key>;

type Ex1                      = Problem<Problem<Test1, "a">, "b"> // No Error
type Ex2<Test2 extends Test1> = Problem<Problem<Test2, "a">, "b"> // Error
//                                                           ^^^ Error occurs here

The specific error message is Type 'string' does not satisfy the constraint 'Exclude<keyof Test2, "a">'. This made me wonder if the issue was that the type argument ("b") was being interpreted as a general string instead of the specific type "b", but some more poking around shows that's not the issue:

type Problem<Base, Key extends keyof Base> = Omit<Base, Key> & { [K in Key]: any };

type Ex2<Test2 extends Test1> = Problem<Problem<Test2, "a">, "b"> // Error
//                                                           ^^^ Error occurs here

For some reason, adding the extra & { [K in Key]: any } bit changes the error message to now specifically say Type '"b"' does not satisfy the constraint '"a" | Exclude<keyof Test2, "a">'. (which may be its own issue because I don't really understand how adding that changes the error message in that way). Regardless, it's my understanding that "b" should satisfy that constraint, since Test2 extends Test1 and therefore must have a key called b.

Weirdly, changing the value of Problem from Omit<Base, Key> to just Base removes the error:

type Test1 = { a: number, b: number };

type Problem<Base, Key extends keyof Base> = Base;

type Ex1                      = Problem<Problem<Test1, "a">, "b"> // No Error
type Ex2<Test2 extends Test1> = Problem<Problem<Test2, "a">, "b"> // No Error

...which suggests to me that even if this is expected behavior, it's still a misplaced error; If the error occurs while trying to satisfy the signature of Omit as opposed to the signature of Problem, then that means there are some types that can be passed as valid arguments to Problem that are not necessarily valid to Omit. Therefore, the type definition for Problem itself should throw an error that its generic type parameters aren't constrained strictly enough to satisfy it's value, in the same way that:

type Problem<Base, Key> = Omit<Base, Key>

...throws an error.

Unsatisfying Fix

One solution is to replace the signature for Problem with

type Problem<Base, Key extends keyof any> = Omit<Base, Key>;

Which now functions as expected, and neither of the two lines error:

type Ex1                      = Problem<Problem<Test1, "a">, "b"> // No Error
type Ex2<Test2 extends Test1> = Problem<Problem<Test2, "a">, "b"> // No Error

However, this comes with the drawback that the second parameter isn't enforced to be a key of the first, i.e., the following throws no error:

type Problem<Base, Key extends keyof any> = Omit<Base, Key>;

type Ex1 = Problem<Test1, "invalid key"> // No Error (this is a bad thing)

This is not desirable; It should be enforceable that Key is a keyof Base.

πŸ™ Actual behavior

Using the type Problem with a generic type parameter passed down gives an error.

πŸ™‚ Expected behavior

I think this is unexpected behavior, and that this shouldn't cause an error. I can't think of a type argument that can be passed to Test2 that doesn't satisfy the constraints needed.

If this is somehow expected behavior, then I think the error is in the wrong place. There's no reason why changing the value of the type Problem without changing its signature should add/remove errors in uses of the type, when the type definition itself has no errors in either case.

Additional information about the issue

No response

vi013t avatar Jun 14 '25 22:06 vi013t

I think this is unexpected behavior, and that this shouldn't cause an error. I can't think of a type argument that can be passed to Test2 that doesn't satisfy the constraints needed.

This is a sort of higher-order argument: We (both you and I) can't think of an instantiation of Ex2 that would violate the constraint. And indeed none exists. But that's the problem -- it's a higher-order argument that depends on be able to reason about the entire universe of types that could inhabit Test2, and being able to correctly derive possible/impossible conditions about that. TypeScript in general isn't equipped to do that, and what we have here just a deferred operation where only a few things are actually known in practice.

In Ex1, where everything's being evaluated immediately, all hypotheticals are irrelevant, nothing is deferred, we're just evaluating concrete types.

If this is somehow expected behavior, then I think the error is in the wrong place. There's no reason why changing the value of the type Problem without changing its signature should add/remove errors in uses of the type, when the type definition itself has no errors in either case.

I don't understand the argument being made here. Of course changing Problem's definition can change its behavior, otherwise its definition is acausal.

RyanCavanaugh avatar Jun 16 '25 19:06 RyanCavanaugh

it's a higher-order argument that depends on be able to reason about the entire universe of types that could inhabit Test2, and being able to correctly derive possible/impossible conditions about that. TypeScript in general isn't equipped to do that,

I'm not completely sure I follow why this limitation exists in this context. The error message indicates that the compiler can't infer that "b" is assignable to "a" | Exclude<keyof Test2, "a">. This should be equivalent to determining if "b" is assignable to keyof Test2. The compiler obviously knows this to be true, because this is possible with no error:

type Test1 = { a: number, b: number };
type Example<Test2 extends Test1> = Test2["b"];

This compiles with no errors, whereas something like Test2["c"] gives an error. So the compiler clearly understands that "b" is a keyof Test2, even though its a generic type parameter. I don't understand how that information isn't enough for the compiler to understand what's going on in the original case.

vi013t avatar Jun 16 '25 23:06 vi013t

How does the compiler know what the output of Exclude<keyof Test2, "a"> is going to be, and that it's definitely going to have "b" in it? It's not correct to just evaluate this in terms of its constraint; this would produce wrong answers.

Remember -- Exclude is just a type alias; the same is true of Omit, these definitions could equally occur in userland under other names.

RyanCavanaugh avatar Jun 16 '25 23:06 RyanCavanaugh

Where specifically is the limitation in Exclude<keyof Test2, "a">? Is with keyof, as in the compiler can't figure out enough about keyof Test2? Or is it in extends (inside the definition for Exclude), where the compiler can't tell that keyof Test2 extends something? It still feels to me like this should be a relatively simple inference for the compiler to make. I understand that Exclude itself is just a type alias, but that just means that these limitations fall on the more fundamental building blocks of it and of this exampleβ€”keyof and extends.

I understand that the compiler can't fully figure out what keyof Test2 will be, but it should be able to tell you that keyof Test2 extends "b" | "a", and that the extends logic that takes place in the definition of Exclude doesn't eliminate "b". I'm having a bit of trouble understanding in which specific syntax rule (keyof or extends) the roadblock is and why.

It's not correct to just evaluate this in terms of its constraint; this would produce wrong answers.

It might help if you could give an example of what you mean here. I get that if Test2 extends { a: number, b: number } then you can't pass keyof Test2 to a function that accepts argument: "a" | "b", because Test2 could have an additional key "c", for example. So keyof Test2 as a type expression can't just directly resolve to its constraint "a" | "b", but I think the compiler could look for specific constructs of keyof Something extends Other and say "while I can't figure out exactly what keyof Something resolves to, I can tell you whether it definitely extends Other". In a case like the specific one mentioned in this issue, that'd mean the compiler saying "yes, I can tell that "b" is definitely in Exclude<keyof Test2, "a">.

vi013t avatar Jun 17 '25 20:06 vi013t

This issue has been marked as "Design Limitation" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

typescript-bot avatar Jun 21 '25 01:06 typescript-bot

Thanks

jemakemal630 avatar Jun 21 '25 03:06 jemakemal630