Repository navigation
Unclear type compatibility of primitives with generic mapped types #47633
Description
Activity
It's a design limitation - conditional types predicated on a generic type parameter are "deferred" (i.e. not actually evaluated) while in a generic context. Essentially, TS doesn't know whether or not
Tis assignable toDeepPartial1<T>because it hasn't resolved the conditional type yet (because it doesn't know whatTis) and thus doesn't know how the input and output types relate--and assumes the worst. In cases like this you should use a type assertion.Reacted by Martin Johns and Andrii DieievBruce Pascoe (@fatcerberus) Ah, interesting. However, that doesn't explain why
DeepPartial3works - why then does TypeScript know thatT[K]can be assigned toT[K] extends object ? DeepPartial3<T[K]> : T[K]? Shouldn't that also be deferred?That I'm not sure of. You would think so though, right? Someone else more knowledgeable about the internal workings of the compiler will need to chime in on that one. 😅. I presume it has something to do with the mapped type being known by the compiler to be homomorphic (
[K in keyof T]) but I might be off-base on that.I do seem to recall there was a special rule where if the compiler can figure out that a type is assignable to both sides of a conditional type, assignment is allowed even while deferred, but I don't know what triggers it.
Ah, I think that's it -
DeepPartial1is distributive, so the "assignable to both sides of the conditional" rule doesn't come into play.Tmight be a union, in which case it would have be evaluated separately for each member of the union, which prevents TS from reasoning about it in the abstract.DeepPartial3doesn't have this problem;T[K] extends ...is not distributive.Consistent with this analysis, making
DeepPartial1non-distributive does make the error go away: See in Playground hereOh, thanks! I didn't know about this neat little trick. Unfortunately, I think I would want my
DeepPartialto be distributive,DeepPartial<number | SomeInterface>should be equivalent tonumber | DeepPartial<SomeInterface>. (Although apparently it is anyway, given{[K in keyof number]?: number[T]}is equivalent tonumberfor some reason - that was my third question).Also I don't understand why the compiler assumes that
Tcan be a union butT[K]can't - but I think that is because it's more of a guessing game than what one would expect in a sound type system?Also I don't understand why the compiler assumes that
Tcan be a union butT[K]can'tIt's not that
T[K]can't be a union - it's thatT[K] extends ...doesn't trigger the distributive behavior (becauseT[K]isn't a bare type parameter), so TS can just go "okay,T[K]will be assignable to both sides of the conditional type regardless of what it is so I'll allow it despite it being deferred." It won't take that shortcut for distributive conditional types because the logic is more complicated (it would have to guarantee assignability individually for any number of input types).Reacted by Andreas BergmaierFor what it's worth, soundness is explicitly not a goal of the type system - it's designed to prevent common errors in idiomatic JS and so you'll find there are a lot of these kinds of guessing games w.r.t. what the rules are. It's been my observation that beyond the basic structural type system which is fairly well-defined for concrete types, things are largely guided by pragmatism (and design constraints) in the more abstract corners of the system.
Yeah, that's what I meant - I know that TypeScript doesn't aim for a sound type system. Unfortunately that imo makes it very unpredictable when attempting to define more advanced precise types for generic functions, a game of try-and-error :-( But I'll stop my rant here and continue trying to learn about all the undocumented intricacies of the compiler implementation… Thanks for your help with that!
Reacted by Bruce Pascoe- addedQuestionAn issue which isn't directly actionable in codeAn issue which isn't directly actionable in code
on Feb 1, 2022 typescript-bot commented
on Feb 3, 2022 ContributorMore actionsThis issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow or the TypeScript Discord community.
- locked as resolved and limited conversation to collaborators
on Oct 22, 2025
Bug Report
🔎 Search Terms
primitive type partial object mapped type
💻 Code
I was trying to define my own
DeepPartialgeneric type mapper. Here are three attempts (and, for comparison, the builtinPartial):⏯ Playground Link
Passing a primitive value to the
examplefunction should correctly infer the same primitive type to be returned.DeepPartial<number>should be equivalent tonumber, not to a mapped object type.This is why I chose implementation 1 with the type condition directly on
T- and callingexample1withstringorstring | numberfor the type parameter does indeed infer that very same type as the result type of the call, unlikeexample2<string | number>(…)returningDeepPartial2<string | number>.🙁 Actual behavior
Using
DeepPartial1, which I thought is the correct implementation, doesn't work. On thereturnstatement of the genericexample1function, tsc complains:(also this message is not very helpful - there's no indication which properties are missing or incompatible)
🙂 Expected behavior
Using
DeepPartial1in the generic function should typecheck just fine.Questions
I don't understand three things:
Tnot being compatible withDeepPartial1<T>- it seems to be a proper subtype to me regardless whetherT extends objector not. Is it a bug or am I missing something?DeepPartial3does work, instead of causing the same problem on the first nesting levelPartial<number>is considered compatible withnumber- it seems that mapped helper types likePartialshould only be used on objects. Why and how does this work, where is this behaviour documented?