You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Comparing constrained generic types/substitution types to conditional types #23132
When comparing a generic type to a conditional type whose checkType is the same type, we have additional information that we are not utilizing.. for instance:
functionf<Textendsnumber>(x: T){vary: Textendsnumber ? number : string;// `T` is not assignable to `T extends number ? number : string`y=x;}
Ignoring intersections, we should be able to take the true branch all the time based on the constraint.
The intuition here is that T in the example above is really T extends number ? T : never which is assignable to T extends number ? number : string.
Similarly, with substitution types, we have additional information that we can leverage, e.g.:
declarefunctionisNumber(a: any): a is number;functionmap<Textendsnumber|string>(o: T,map: (value: Textendsnumber ? number : string)=>any): any{if(isNumber(o)){// `T & number` is not assignable to `T extends number ? number : string`returnmap(o);}}
Mohamed Hegazy (@mhegazy)
In the second example: is the reason that T & number should be assignable to T extends number ? number : stringbecause the bound of the conditional is number | string, and the intersection includes number?[1] Or is it due to some combination of the type-variable and the intersection?
[1] Counter example: pick T to be never. The intersection needs to include T.
That is the type that results narrowing the type of o to number. it becomes T & number. which should behave, as i noted in the OP, as T extends number ? T : never, which is assignable to T extends number ? number : string
Sorry I misunderstood. I thought that 'behaving as T extends number ? T : never' only referred to the top case when T extends number, and that perhaps there was a subtle difference with the intersection.
As in the top case the constraint applies to the type T, but in the bottom case the evidence only applies to the value, the constraint for T in general cannot be narrowed.
My use case is that I want to have a generic base class that that does full discriminated union checking based on the generic type and then dispatches to an abstract method in a fully-discriminated way. This makes it so I can write a very simple/trim handler class which is valuable when you have 100s of things to discriminate between. In this particular case, there are multiple discriminations along the way that need to "aggregate" into a fully narrowed type, which is why we run into this bug.
Note: ClientHandler can further be generalized into something like the following which would allow us to use the same discriminating base class for both Client and Server handlers by just including the direction in the class declaration. I chose to leave out this generalization in this issue to avoid making things too clouded.
Note: While it may not appear so at first glance, the error received here reduces down to this bug. After deleting code until I had nothing left but the simplest repro case I ended up with #32591, which appears to be a duplicate of this.
Note: The excessive usage of types here is to ensure that we get type checking and it is really hard to create a new request/response pair without properly implementing everything. The goal is to make it so a developer showing up to the project can create a new class CherryHandler extends ClientHandler<CherryMessage> and then get compile errors until they have done all of the work necessary (including creating all of the necessary types) to make that handler work properly.
interfaceBaseChannel{channel: Message['channel']}interfaceBaseRequest{direction: 'request'}interfaceBaseResponse{direction: 'response'}typeRequestMessage=Extract<Message,BaseRequest>typeResponseMessage=Extract<Message,BaseResponse>typeMessage=AppleMessage|BananaMessageconstisRequestMessage=(maybe: Message): maybe is RequestMessage=>maybe.direction==='request'constisResponseMessage=(maybe: Message): maybe is ResponseMessage=>maybe.direction==='response'abstractclassClientHandler<TextendsMessage>{receive=(message: Message)=>{if(!isResponseMessage(message))returnif(!this.isT(message))return// Type 'AppleResponse & T' is not assignable to type 'Extract<T, AppleResponse>'.this.onMessage(message)// error}abstractonMessage: (message: Extract<T,ResponseMessage>)=>voidabstractchannel: T['channel']privatereadonlyisT=(maybe: Message): maybe is T=>maybe.channel===this.channel}interfaceAppleChannelextendsBaseChannel{channel: 'apple'}interfaceAppleRequestextendsBaseRequest,AppleChannel{}interfaceAppleResponseextendsBaseResponse,AppleChannel{}typeAppleMessage=AppleRequest|AppleResponseclassAppleHandlerextendsClientHandler<AppleMessage>{// we'll get a type error here if we put anything other than 'apple'channel='apple'asconst// notice that we get an AppleResponse here, because we already fully discriminated in the base classonMessage=(message: AppleResponse): void=>{// TODO: handle AppleResponse}}interfaceBananaChannelextendsBaseChannel{channel: 'banana'}interfaceBananaRequestextendsBaseRequest,BananaChannel{}interfaceBananaResponseextendsBaseResponse,BananaChannel{}typeBananaMessage=BananaRequest|BananaResponseclassBananaHandlerextendsClientHandler<BananaMessage>{channel='banana'asconstonMessage=(message: BananaResponse): void=>{}}
This issue keeps biting me over and over in this project. I wish I could thumbs-up once for each time I suffer from the fact that {a:any} is a valid generic instantiation of T extends {a:'a'}.
Latest is basically this (greatly simplified):
functionfun<TextendsUnion>(kind: T['kind']){constunion: Union={ kind }// Type '{ kind: T["kind"]; }' is not assignable to type 'Union'.}fun<{kind: any}>({kind: 5})// it is crazy to me that this line is valid
I want to be able to tell the compiler, "any should be treated as unknown and is not a valid extension of string (or anything else)".
Ryan Cavanaugh (@RyanCavanaugh) another legit example of this, I'm creating a rxjs operator that returns an instance of a value if the stream is not an array, or an array of instances if it is. I have to use any in the array branch to make it work. (Observable types were removed, but the main use case, and the problem, still remains)
I'm having the same issues with this and what might help is the below:
functionGetThing<T>(default:T,value:unknown): T|void{if(typeofdefault==="boolean")return!!value;// Type 'boolean' is not assignable to type 'void | T'. ts(2322)}// ORfunctionGetThing<T>(default:T,value:unknown): T{if(typeofdefault==="boolean")return!!value;// Type 'boolean' is not assignable to type 'T'.// 'boolean' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{}'. ts(2322)// etc.}
Those just simplify the reproduction.
However one of the more interesting ones might be this one:
functionGetThing<T>(default:T,value:unknown): T{if(typeofdefault==="boolean")return(!!value)as{};/Type'{}'isnotassignabletotype'T'.
// '{}' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{}'.ts(2322)}
EDIT: MAde a bunch of mistakes when copying this over
Gianthra Despite me being the one to refer you to this thread, I think I was mistaken originally and your problem is actually different. In your case, the problem lies in the fact that T could be a more constrained type than boolean. This will cause typeof default === 'boolean' to resolve to true, but T may be the type false.
Some of these issues seem related to #13995#33014 / #33912 ... if an unresolved generic type parameter could be narrowed to something concrete inside a function implementation, the conditional type would also be resolved.
I also stumbled upon another valid use case: dynamic restrictions based on some attribute.
In my particular case, I'm trying to build an UI library based on Command/Handler approach, where we can model actions in form of data that will be later interpreted and executed by a dynamic handler.
Action is the base on which all the others are built, while TagR is a restriction that will allow to use certain actions only on specific html elements. Such an action could be adding an onInput event handler, which only makes sense for input and textarea, (etc...) elements:
Please note that restriction field is set to TagR<'input'|'textarea'>, as we said before.
Next we need an action that models an element:
constElementType=Symbol()typeElementAction<Textendsstring,AextendsAction>={type: typeofElementType,tag: T,actions: A[]restriction: ElemR<A>}typeElemR<AextendsAction>=UtoI<RemoveRestr<TagR<any>,A['restriction']>>typeRemoveRestr<R,AR>=Pick<AR,Exclude<keyofAR,keyofR>>// From Union to Intersaction: UtoI<A | B> = A & B typeUtoI<U>=(Uextendsany ? (k: U)=>void : never)extends((k: infer I)=>void) ? I : never;
The most interesting part is RemoveRestr, which will allow restrictions to bubble up, so that some other action can take care of it, allowing an implementation similar to React Context but much more type-safe.
This however is only half of the picture, the last piece is in the action creator:
consth=<Textendsstring,AextendsAction>(tag: T,actions: MatchRestr<TagR<T>,A>[]): ElementAction<T,A,ElemR<A>>=>TODO()typeMatchRestr<R,A>=AextendsAction<any, infer AR>
? RextendsPick<AR,Extract<keyofAR,keyofR>> ? A : never
: never
MatchRestr will ensure that only actions that either matches it or do not require this kind of restriction will be assignable.
So, given all this code we now have that:
h('input',[onInput(TODO)])
Works as expected given that restrictions matches
h('div',[h('input',[onInput(TODO)]),h('br',[])])
Works too given that element do not require any constraint and rather remove the onInput one.
h('div',[onInput(TODO)])
This will raise a type error!
So far, so good. The problem arise as soon as we try to abstract some of it. Let's say that we want a wrapper element and it should only receives other children elements:
Argument of type 'E[]' is not assignable to parameter of type 'MatchRestr<TagR<"div">, E>[]'.
Type 'E' is not assignable to type 'MatchRestr<TagR<"div">, E>'.
Type 'ElementAction<any, any>' is not assignable to type 'MatchRestr<TagR<"div">, E>'.
^ this I initially didn't expect, but anyway tried to solve it like this:
Type 'MatchRestr<TagR<any>, E>' is not assignable to type 'OnInputAction | MatchRestr<TagR<"input">, MatchRestr<TagR<any>, E>>'.
Type 'Action<any, {}> & E' is not assignable to type 'OnInputAction | MatchRestr<TagR<"input">, MatchRestr<TagR<any>, E>>'.
Type 'ElementAction<any, any>' is not assignable to type 'OnInputAction | MatchRestr<TagR<"input">, MatchRestr<TagR<any>, E>>'.
Type 'ElementAction<any, any>' is not assignable to type 'MatchRestr<TagR<"input">, MatchRestr<TagR<any>, E>>'.
Type 'Action<any, {}> & E' is not assignable to type 'OnInputAction'.
Type 'ElementAction<any, any>' is not assignable to type 'OnInputAction | MatchRestr<TagR<"input">, MatchRestr<TagR<any>, E>>'.
Type 'ElementAction<any, any>' is not assignable to type 'MatchRestr<TagR<"input">, MatchRestr<TagR<any>, E>>'.
Type 'MatchRestr<TagR<any>, E>' is not assignable to type 'OnInputAction'.
Property 'handler' is missing in type 'Action<any, {}> & ElementAction<any, any>' but required in type 'OnInputAction'.
Property 'handler' is missing in type 'ElementAction<any, any>' but required in type 'OnInputAction'.
Not sure why it infers as Action<any, {}> & E when it starts as OnInputAction | MatchRestr<TagR<"input">, MatchRestr<TagR<any>, E>>.
Please note that explicitly setting the type variables will solve the problem:
I am working on my API hook in nextjs project but facing this kind of issue.
interface User {
name: string;
}
interface Admin {
name: string;
age: number;
}
interface Auth {
user: User;
admin: Admin;
}
const Auth = <Name extends keyof Auth>(name: Name, body: Auth[Name]) => {
if (name == "admin") {
// here typeof body must be Auth['admin'] but it not give any suggestion .. only gives body.name
}
};
Ryan Cavanaugh (@RyanCavanaugh) not sure if that's true... I have just tried what you wrote in the playground and it yields an error. Am I missing something?
I am working on my API hook in nextjs project but facing this kind of issue.
interface User {
name: string;
}
interface Admin {
name: string;
age: number;
}
interface Auth {
user: User;
admin: Admin;
}
const Auth = <Name extends keyof Auth>(name: Name, body: Auth[Name]) => {
if (name == "admin") {
// here typeof body must be Auth['admin'] but it not give any suggestion .. only gives body.name
}
};
function isFoxy<T>(x: T, flag: T extends object ? string : number) {
console.log(`hmm, not sure`, x, flag)
}
type Fox = { fox: string }
function foxify<F extends Fox>(fox: F) {
isFoxy(fox, 'yes') // ❌ type error here
}
This is a contrived example, obviously, but only slightly less complicated than the real-world one I'm facing.
Activity
jack-williams commentedon Apr 4, 2018
Mohamed Hegazy (@mhegazy)
In the second example: is the reason that
T & numbershould be assignable toT extends number ? number : stringbecause the bound of the conditional is[1] Or is it due to some combination of the type-variable and the intersection?number | string, and the intersection includesnumber?[1] Counter example: pick
Tto benever. The intersection needs to includeT.mhegazy commentedon Apr 4, 2018
That is the type that results narrowing the type of
otonumber. it becomesT & number. which should behave, as i noted in the OP, asT extends number ? T : never, which is assignable toT extends number ? number : stringjack-williams commentedon Apr 4, 2018
Sorry I misunderstood. I thought that 'behaving as
T extends number ? T : never' only referred to the top case whenT extends number, and that perhaps there was a subtle difference with the intersection.As in the top case the constraint applies to the type
T, but in the bottom case the evidence only applies to the value, the constraint forTin general cannot be narrowed.RyanCavanaugh commentedon Apr 17, 2018
Looking for concrete examples of where this comes up more legitimately
kevinbeal commentedon May 1, 2018
Ryan Cavanaugh (@RyanCavanaugh) I think I have one.
From #23803
I think Mohamed Hegazy (@mhegazy) 's alternative solution there may work though.
mattmccutchen commentedon Aug 3, 2018
Two semi-legitimate examples from #25883:
Andarist commentedon Mar 2, 2019
My legitimate (I think 😉 ) use case:
Having a mapped type which tests against
undefinedwith conditional type, can't perform that with constrained generic - see playgroundMicahZoltu commentedon Jul 30, 2019
My use case is that I want to have a generic base class that that does full discriminated union checking based on the generic type and then dispatches to an abstract method in a fully-discriminated way. This makes it so I can write a very simple/trim handler class which is valuable when you have 100s of things to discriminate between. In this particular case, there are multiple discriminations along the way that need to "aggregate" into a fully narrowed type, which is why we run into this bug.
Note:
ClientHandlercan further be generalized into something like the following which would allow us to use the same discriminating base class for both Client and Server handlers by just including the direction in the class declaration. I chose to leave out this generalization in this issue to avoid making things too clouded.Note: While it may not appear so at first glance, the error received here reduces down to this bug. After deleting code until I had nothing left but the simplest repro case I ended up with #32591, which appears to be a duplicate of this.
Note: The excessive usage of types here is to ensure that we get type checking and it is really hard to create a new request/response pair without properly implementing everything. The goal is to make it so a developer showing up to the project can create a new
class CherryHandler extends ClientHandler<CherryMessage>and then get compile errors until they have done all of the work necessary (including creating all of the necessary types) to make that handler work properly.MicahZoltu commentedon Aug 6, 2019
This issue keeps biting me over and over in this project. I wish I could thumbs-up once for each time I suffer from the fact that
{a:any}is a valid generic instantiation ofT extends {a:'a'}.Latest is basically this (greatly simplified):
I want to be able to tell the compiler, "
anyshould be treated asunknownand is not a valid extension ofstring(or anything else)".michaeljota commentedon Aug 7, 2019
Ryan Cavanaugh (@RyanCavanaugh) another legit example of this, I'm creating a rxjs operator that returns an instance of a value if the stream is not an array, or an array of instances if it is. I have to use
anyin the array branch to make it work. (Observable types were removed, but the main use case, and the problem, still remains)Playground
Gianthra commentedon Aug 19, 2019
I'm having the same issues with this and what might help is the below:
Those just simplify the reproduction.
However one of the more interesting ones might be this one:
EDIT: MAde a bunch of mistakes when copying this over
MicahZoltu commentedon Aug 19, 2019
Gianthra Despite me being the one to refer you to this thread, I think I was mistaken originally and your problem is actually different. In your case, the problem lies in the fact that
Tcould be a more constrained type thanboolean. This will causetypeof default === 'boolean'to resolve totrue, butTmay be the typefalse.jcalz commentedon Aug 27, 2019
Some of these issues seem related to
#13995#33014 / #33912 ... if an unresolved generic type parameter could be narrowed to something concrete inside a function implementation, the conditional type would also be resolved.iazel commentedon Oct 23, 2019
I also stumbled upon another valid use case: dynamic restrictions based on some attribute.
In my particular case, I'm trying to build an UI library based on Command/Handler approach, where we can model actions in form of data that will be later interpreted and executed by a dynamic handler.
Example:
Actionis the base on which all the others are built, whileTagRis a restriction that will allow to use certain actions only on specific html elements. Such an action could be adding anonInputevent handler, which only makes sense forinputandtextarea, (etc...) elements:Please note that
restrictionfield is set toTagR<'input'|'textarea'>, as we said before.Next we need an action that models an element:
The most interesting part is
RemoveRestr, which will allow restrictions to bubble up, so that some other action can take care of it, allowing an implementation similar to React Context but much more type-safe.This however is only half of the picture, the last piece is in the action creator:
MatchRestrwill ensure that only actions that either matches it or do not require this kind of restriction will be assignable.So, given all this code we now have that:
Works as expected given that restrictions matches
Works too given that
elementdo not require any constraint and rather remove theonInputone.This will raise a type error!
So far, so good. The problem arise as soon as we try to abstract some of it. Let's say that we want a wrapper element and it should only receives other children elements:
This raises a type-error:
^ this I initially didn't expect, but anyway tried to solve it like this:
And it indeed works, however as soon as I try to add something different it will break again:
Not sure why it infers as
Action<any, {}> & Ewhen it starts asOnInputAction | MatchRestr<TagR<"input">, MatchRestr<TagR<any>, E>>.Please note that explicitly setting the type variables will solve the problem:
Here is it in the Playground
husnain129 commentedon Jul 20, 2022
I am working on my API hook in nextjs project but facing this kind of issue.
RyanCavanaugh commentedon Jul 20, 2022
Muhammad Husnain (@husnain129) not a correct assumption,
Auth<"name" | "admin">("admin", "hello")is a legal calljuona commentedon Aug 11, 2022
Ryan Cavanaugh (@RyanCavanaugh) not sure if that's true... I have just tried what you wrote in the playground and it yields an error. Am I missing something?
Playground
RyanCavanaugh commentedon Aug 16, 2022
Typo, should be
theabdulmateen commentedon Nov 29, 2022
A 'workaround' would be to use the
iskeyword.edited playground
githorse commentedon Sep 7, 2023
Another simple illustration at this StackOverflow question:
This is a contrived example, obviously, but only slightly less complicated than the real-world one I'm facing.