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
π» 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
WorksandFailsbehave differentlyTestFails2results in an error
π Expected behavior
WorksandFailsbehave the sameTestFails2does 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):

π€ 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
- (73%) microsoft/typescript#40670: Regression in 4.0 with keyof on partial types
- (70%) microsoft/typescript#57690: Narrowed type not "carried" iteratively
- (70%) microsoft/typescript#53498: Generic type allows unrelated types in 5.0.2
- (69%) microsoft/typescript#55757: `Readonly` type loses `Partial`-ness of generic-keyed `Record`
- (69%) microsoft/typescript#61242: `T extends unknown` is observably-different from an unconstrained `T`
- (69%) microsoft/typescript#60290: Generic types extending Record<Uppercase<string>, ...> ignore the Uppercase utility
- (69%) microsoft/typescript#28839: Strange keyof behavior with combination of Partial and Record.
- (69%) microsoft/typescript#40029: TypeScript conditional types constrain no longer working after v3.5.1
- (69%) microsoft/typescript#48074: Instantiating record entries with a generic key is not possible when this key extends string. It is when the key extends number.
- (69%) microsoft/typescript#52612: Generic constraints cannot be refactored in some scenarios
- (68%) microsoft/typescript#57693: `null` gets accidentally eliminated when narrowing by undefined's equality
- (68%) microsoft/typescript#49577: Seemingly-fine code produces an error in 4.7 but not 4.6
- (68%) microsoft/typescript#32017: 3.5 regression: invalid type for index lookup using constrained generic
- (68%) microsoft/typescript#38601: `Record<T[K], T[]>` does not compile anymore
- (68%) microsoft/typescript#42469: Can assign incompatible object to Partial<Record>, why?
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.