ARTICLE AD BOX
The general problem is that TypeScript can't really do too much analysis when you "mix" generics with conditional types. Once a generic type parameter is specified with a specific type argument, TypeScript can just evaluate the conditional type to get the resulting type, but when there's an unresolved type parameter, it mostly just leaves the type deferred and doesn't try too much.
So inside the body of your generic functions TypeScript will disappoint you when it is unable to perform higher-order analysis. The body of updateNoBExtender() is like this. If you need this sort of function, you'll probably have to use type assertions or the like inside the body.
And also, when you try to call a generic function where the generic type parameter is only mentioned as the argument of a conditional type, TypeScript fails to infer a type argument. in order for TypeScript to infer M from a value of type FullExtender<M>, it would need to reverse the definition of FullEntender<M>. The question "for which possible M is FullExtender<M> assignable to FullExtender<'Extender1'>" seems like it has an obvious answer by pattern matching, but FullExtender<'Extender1'> is immediately evaluated as Extender1. And so now you're asking "for which possible M is FullExtender<M> assignable to Extender1, and TypeScript effectively gives up. It does not iterate through various possible M candidates. Instead it says "I have no idea from where I should infer this" and just falls back to the constraint ExtenderMode.
In order to proceed I would generally try to:
avoid generic conditional types anywhere they are likely to be used with the generic type parameter unresolved, and only try to infer generic type arguments from values very directly related to the relevant generic type parameters. Inferring M from FullExtender<M> is hard, inferring F from F is easy.So here's a possible refactoring of your example code:
type FullExtender = Extender1 | Extender2 function updateFullExtender<F extends FullExtender, K extends keyof F>( extender: F, key: K, value: F[K],): F { return { ...extender, [key]: value }; } function setFullExtenderAToThing<F extends FullExtender>(extender: F): F { //updateFullExtender(extender, 'c1', 'thing'); // fails as expected return updateFullExtender(extender, 'a', 'thing'); // works as expected } const specificFullExtender: Extender1 = { a: 'a', b: 'b', c1: 'c1' }; setFullExtenderAToThing(specificFullExtender); updateFullExtender(specificFullExtender, 'a', 'thing again'); updateFullExtender(specificFullExtender, 'c1', 'thing again'); type DistribOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never type FullExtenderNoB = DistribOmit<FullExtender, "b">; function updateNoBExtender<F extends FullExtenderNoB, K extends keyof F>( extender: F, key: K, value: F[K]): F { return { ...extender, [key]: value }; } function setNoBAToThing<F extends FullExtenderNoB>(extender: F): F { return updateNoBExtender(extender, 'a', 'thing'); // works } const specificNoBExtender: Omit<Extender1, 'b'> = { a: 'a', c1: 'c1' }; setNoBAToThing(specificNoBExtender); updateNoBExtender(specificNoBExtender, 'a', 'no thing'); updateNoBExtender(specificNoBExtender, 'c1', 'no thing');What I've done here is give up completely on M since it's just a nearly arbitrary label that complicates things. There are definitely use cases where such labels are useful, but I don't see it here. If that happens you might need to use the approach as described in microsoft/TypeScript#47109 and various answers I've posted on Stack Overflow that mention it.
Instead of trying to infer the label from some extender, we just infer the extender from itself. That's F. Now much of the code just starts to work. Yes, it's possible that F might be some subtype of FullExtender that you didn't expect, but the basic spreading code mostly works for those cases too. If you run into a use case where you need to prohibit a certain input for updateFullExtender() and updateNoBExtender(), you can probably do that with this same general approach.
The DistribOmit thing is just a convenience type to produce FullExtenderNoB but you could write that out yourself. Again, updateNoBExtender just uses F but the constraint is FullExtenderNoB. Yes, this might allow some inputs you don't expect, but again, the basic spreading code should still work there.
So that's my first-order refactoring of this code. If you ever find yourself needing to restrict a function call to some enumerated list of string literal related types, microsoft/TypeScript#47109 is the way to do that. But you'd probably better make sure those string literals are actually present in the arguments to your function. So the call would look like f("Extender1") or g({type: "Extender1"}). Otherwise it's likely to be just too much indirection.
