Skip to content

Unclear type compatibility of primitives with generic mapped types #47633

Description

Bug Report

🔎 Search Terms

primitive type partial object mapped type

💻 Code

I was trying to define my own DeepPartial generic type mapper. Here are three attempts (and, for comparison, the builtin Partial):

export type DeepPartial1<T> = T extends object ? { [K in keyof T]?: DeepPartial1<T[K]> } : T;
export type DeepPartial2<T> = { [K in keyof T]?: DeepPartial2<T[K]> };
export type DeepPartial3<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial3<T[K]> : T[K] };

function example0<T>(x: T): Partial<T> {
    return x;
}
function example1<T>(x: T): DeepPartial1<T> {
    return x;
}
function example2<T>(x: T): DeepPartial2<T> {
    return x;
}
function example3<T>(x: T): DeepPartial3<T> {
    return x;
}

⏯ Playground Link

Passing a primitive value to the example function should correctly infer the same primitive type to be returned. DeepPartial<number> should be equivalent to number, not to a mapped object type.
This is why I chose implementation 1 with the type condition directly on T - and calling example1 with string or string | number for the type parameter does indeed infer that very same type as the result type of the call, unlike example2<string | number>(…) returning DeepPartial2<string | number>.

🙁 Actual behavior

Using DeepPartial1, which I thought is the correct implementation, doesn't work. On the return statement of the generic example1 function, tsc complains:

Type 'T' is not assignable to type 'DeepPartial1<T>'.(2322)

(also this message is not very helpful - there's no indication which properties are missing or incompatible)

🙂 Expected behavior

Using DeepPartial1 in the generic function should typecheck just fine.

Questions

I don't understand three things:

  • why the compiler complains about T not being compatible with DeepPartial1<T> - it seems to be a proper subtype to me regardless whether T extends object or not. Is it a bug or am I missing something?
  • why the similar construction in DeepPartial3 does work, instead of causing the same problem on the first nesting level
  • why Partial<number> is considered compatible with number - it seems that mapped helper types like Partial should only be used on objects. Why and how does this work, where is this behaviour documented?

Activity

  1. fatcerberus commented on Jan 27, 2022

    @fatcerberus

    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 T is assignable to DeepPartial1<T> because it hasn't resolved the conditional type yet (because it doesn't know what T is) 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.

  2. ab-pm commented on Jan 27, 2022

    @ab-pm
    Author

    Bruce Pascoe (@fatcerberus) Ah, interesting. However, that doesn't explain why DeepPartial3 works - why then does TypeScript know that T[K] can be assigned to T[K] extends object ? DeepPartial3<T[K]> : T[K]? Shouldn't that also be deferred?

  3. fatcerberus commented on Jan 27, 2022

    @fatcerberus

    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.

  4. fatcerberus commented on Jan 27, 2022

    @fatcerberus

    Ah, I think that's it - DeepPartial1 is distributive, so the "assignable to both sides of the conditional" rule doesn't come into play. T might 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. DeepPartial3 doesn't have this problem; T[K] extends ... is not distributive.

    Consistent with this analysis, making DeepPartial1 non-distributive does make the error go away: See in Playground here

  5. ab-pm commented on Jan 27, 2022

    @ab-pm
    Author

    Oh, thanks! I didn't know about this neat little trick. Unfortunately, I think I would want my DeepPartial to be distributive, DeepPartial<number | SomeInterface> should be equivalent to number | DeepPartial<SomeInterface>. (Although apparently it is anyway, given {[K in keyof number]?: number[T]} is equivalent to number for some reason - that was my third question).

    Also I don't understand why the compiler assumes that T can be a union but T[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?

  6. fatcerberus commented on Jan 27, 2022

    @fatcerberus

    Also I don't understand why the compiler assumes that T can be a union but T[K] can't

    It's not that T[K] can't be a union - it's that T[K] extends ... doesn't trigger the distributive behavior (because T[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).

  7. fatcerberus commented on Jan 27, 2022

    @fatcerberus

    For 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.

  8. ab-pm commented on Jan 27, 2022

    @ab-pm
    Author

    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!

  9. typescript-bot commented on Feb 3, 2022

    @typescript-bot
    Contributor

    This 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.

  10. locked as resolved and limited conversation to collaborators on Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    QuestionAn issue which isn't directly actionable in code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions