microsoft/TypeScript

Generic constraint fails with `Partial<Record<keyof T, any>>` but works with `{ [K in keyof T]?: any }`

Closed this issue Β· 5 comments

πŸ”Ž Search Terms

partial record mapped type generic constraint

πŸ•— Version & Regression Information

  • This changed between versions 3.9.7 and 4.0.5

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.9.3#code/C4TwDgpgBA6g9gJwNYGcA8AVKEAewIB2AJilHAEYBWEAxsADRQCq2ehJUA3lANoDSUAJYEoSCCDgAzKBgC6AfgBcUAIYEQUAL4A+KAF5mAbgBQoSFABiKwQBt0WXPmKkK1OoxaP2pAAoqEwIIqNmgASrSIRGhiEtIYjGog2sn6Rsam4NAAggBCqZwqyijACMIA5ozkisWlBGWaGeZ5BpxVUDXlDcYA9N0yABYQKNDkcMD9UADuiEiqxNgAbsEArir4UMBwG4PtKgC20GYQjdAYQ8DwyCgAjKmXqGi5jDnaJzLnVrY3qZ92jznPV49PoAOS2EAQCEQKDeZ2K9xQACZMKwnBxcroDAjMIDgVAAKKQxDKDCZKAAchy5KgRDgQygBDGu0CKEkGnG0BocAINWsBGAFL8ASCIXCXIQURiUhkCXUyXJADpYR9rHZkQ42M4oBifqr7LigA

πŸ’» Code

type Works<T extends object, U extends { [K in keyof T]?: any }> = U;
type Fails<T extends object, U extends Partial<Record<keyof T, any>>> = U;

type AB = {a: string, b:string}
type B = {b: string}

// These both work and evaluate to the same type
type TestWorks1 = Works<AB, B>
type TestFails1 = Fails<AB, B>

// No errors
type TestWorks2<T extends AB> = Works<T, B>
// Error: Type 'B' does not satisfy the constraint 'Partial<Record<keyof T, any>>'.
type TestFails2<T extends AB> = Fails<T, B>

πŸ™ Actual behavior

  • Works and Fails behave differently
  • TestFails2 results in an error

πŸ™‚ Expected behavior

  • Works and Fails behave the same
  • TestFails2 does not result in an error

Additional information about the issue

@easrng narrowed down the divergence in behavior between the two types to this line in checker.ts (screenshot because the file is too big for github to display):

Image

πŸ€– Thank you for your issue! I've done some analysis to help get you started. This response is automatically generated; feel free to πŸ‘ or πŸ‘Ž this comment according to its usefulness.

Possible Relevant FAQs

Structural vs Instantiation-Based Inference

The two constraint spellings look equivalent, but they reach the relation algorithm through different paths.

β€’ { [K in keyof T]?: any } is a freshly-created mapped type. When the checker has to decide whether B satisfies that constraint, it performs a purely structural comparison: it expands the mapped type, sees that every keyof T is optional with an any value, and concludes that B is acceptable.

β€’ Partial<Record<keyof T, any>> is an instantiation of two generic aliases (Record then Partial). When the checker compares B to that constraint it first tries the faster instantiation-based inference: it notices that the target is a Partial<…> but can’t match B back to the exact instantiation Partial<Record<keyof T, any>> because T is still generic at that point. It therefore stops before falling back to a property-by-property comparison, and the constraint fails.

This difference is an intentional implementation detail: TypeScript prefers instantiation-based inference whenever it can because it is considerably faster, even though the result can diverge from a fully structural walk in edge cases like this. Whether the compiler chooses the structural or instantiation route is not something to rely on; it can change between versions as performance and correctness work continues.

Practically, use the mapped-type form if the constraint must hold while T is still generic:

// always succeeds
type Constrained<T extends object, U extends { [K in keyof T]?: any }> = U;

or, if the Record spelling is preferred, delay keyof T so that T is concrete when the check happens (e.g. apply the constraint outside the generic alias).

Similar Issues

Here are the most similar issues I found

If your issue is a duplicate of one of these, feel free to close this issue. Otherwise, no action is needed.

When we have a mapped type like { [K in keyof T]?: any } there are some determinations that can be made about it, e.g. "does this type have these keys in it (yes)", when T is constrained. That's what the screenshotted code is doing - taking advantage of having more information about what kinds of type could be produced from an arbitrary concrete T.

When there's a more complex instantiation like Partial<Record<keyof T, any>> where T is still a type parameter, that's a fundamentally different situation. There's not really a notion of a "speculative instantiation" where we produce some type that actually is Partial<Record<keyof some-constraint-here, any>> from which we can make useful determinations.

To be fair, I'm running low on sleep today so I won't claim I have considered everything around it. But the fast path like this is usually merely an optimization - and the intuition says to me that following the alternative slow path should yield the same results. In here, following that slow path actually fixes the issue. I put a PR for this as an experiment here: #62724

Also, as mentioned by the OP - this worked in TS 3.9 fine (TS playground). That's before this fast path was introduced: #39696

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