Open mhegazy opened 6 years ago
@mhegazy
In the second example: is the reason that T & number
should be assignable to T extends number ? number : string
because 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 includes number
?
[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.
Looking for concrete examples of where this comes up more legitimately
@RyanCavanaugh I think I have one.
interface ControlResult<T> {
fields: ConvertPrimitiveTo<T, FormControl>;
controls: T extends any[] ? FormArray : FormGroup;
}
From #23803
I think @mhegazy 's alternative solution there may work though.
Two semi-legitimate examples from #25883:
My legitimate (I think 😉 ) use case:
Having a mapped type which tests against undefined
with conditional type, can't perform that with constrained generic - see playground%0D%0A%20%20%20%20%26%20((type%3A%20GetUndefined%3CTypesMap%3E)%20%3D%3E%20void)%0D%0A%0D%0A%0D%0Adeclare%20const%20createEmit%3A%20%3CTypesMap%20extends%20AnyMap%3E()%20%3D%3E%20Emit%3CTypesMap%3E%0D%0A%0D%0Atype%20Test%3CT%3E%20%3D%20%7B%0D%0A%20%20%20%20foo%3A%20string%0D%0A%20%20%20%20bar%3A%20undefined%0D%0A%20%20%20%20xyz%3A%20T%0D%0A%7D%20%0D%0A%0D%0Afunction%20foo%3CT%20extends%20string%20%7C%20number%3E()%20%7B%0D%0A%20%20%20%20const%20emit%20%3D%20createEmit%3CTest%3CT%3E%3E()%0D%0A%20%20%20%20emit('foo'%2C%20'test')%0D%0A%20%20%20%20emit('bar')%0D%0A%20%20%20%20%2F%2F%20this%20should%20know%20that%20it%20couldnt%20resolve%20to%20undefined%0D%0A%20%20%20%20%2F%2F%20because%20T%20is%20constrained%20to%20string%20%7C%20number%0D%0A%20%20%20%20emit('xyz'%2C%20'da')%0D%0A%7D%0D%0A%0D%0A)
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.
Handler<T extends Message, U extends { direction: 'request' | 'response' }>
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.
interface BaseChannel { channel: Message['channel'] }
interface BaseRequest { direction: 'request' }
interface BaseResponse { direction: 'response' }
type RequestMessage = Extract<Message, BaseRequest>
type ResponseMessage = Extract<Message, BaseResponse>
type Message = AppleMessage | BananaMessage
const isRequestMessage = (maybe: Message): maybe is RequestMessage => maybe.direction === 'request'
const isResponseMessage = (maybe: Message): maybe is ResponseMessage => maybe.direction === 'response'
abstract class ClientHandler<T extends Message> {
receive = (message: Message) => {
if (!isResponseMessage(message)) return
if (!this.isT(message)) return
// Type 'AppleResponse & T' is not assignable to type 'Extract<T, AppleResponse>'.
this.onMessage(message) // error
}
abstract onMessage: (message: Extract<T, ResponseMessage>) => void
abstract channel: T['channel']
private readonly isT = (maybe: Message): maybe is T => maybe.channel === this.channel
}
interface AppleChannel extends BaseChannel { channel: 'apple' }
interface AppleRequest extends BaseRequest, AppleChannel { }
interface AppleResponse extends BaseResponse, AppleChannel { }
type AppleMessage = AppleRequest | AppleResponse
class AppleHandler extends ClientHandler<AppleMessage> {
// we'll get a type error here if we put anything other than 'apple'
channel = 'apple' as const
// notice that we get an AppleResponse here, because we already fully discriminated in the base class
onMessage = (message: AppleResponse): void => {
// TODO: handle AppleResponse
}
}
interface BananaChannel extends BaseChannel { channel: 'banana' }
interface BananaRequest extends BaseRequest, BananaChannel { }
interface BananaResponse extends BaseResponse, BananaChannel { }
type BananaMessage = BananaRequest | BananaResponse
class BananaHandler extends ClientHandler<BananaMessage> {
channel = 'banana' as const
onMessage = (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):
function fun<T extends Union>(kind: T['kind']) {
const union: 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)".
@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:
function GetThing<T>(default:T, value:unknown): T | void {
if(typeof default === "boolean") return !!value; // Type 'boolean' is not assignable to type 'void | T'. ts(2322)
}
// OR
function GetThing<T>(default:T, value:unknown): T {
if(typeof default === "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:
function GetThing<T>(default:T, value:unknown): T {
if(typeof default === "boolean") return (!!value) as {};
/ Type '{}' is not assignable to type '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.
Example:
type Action<T extends symbol = any, R extends {} = any> = {
type: T
restriction: R
}
type TagR<T extends string> = {tag: T}
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:
const OnInputType = Symbol()
type EventHandler = (ev: InputEvent) => void
type OnInputAction = {
type: typeof OnInputType,
handler: EventHandler,
restriction: TagR<'input' | 'textarea'>
}
const onInput = (handler: EventHandler): OnInputAction => TODO
Please note that restriction
field is set to TagR<'input'|'textarea'>
, as we said before.
Next we need an action that models an element:
const ElementType = Symbol()
type ElementAction<T extends string, A extends Action> = {
type: typeof ElementType,
tag: T,
actions: A[]
restriction: ElemR<A>
}
type ElemR<A extends Action> = UtoI<
RemoveRestr<TagR<any>, A['restriction']>
>
type RemoveRestr<R, AR> =
Pick<AR, Exclude<keyof AR, keyof R>>
// From Union to Intersaction: UtoI<A | B> = A & B
type UtoI<U> =
(U extends any ? (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:
const h = <T extends string, A extends Action>(
tag: T,
actions: MatchRestr<TagR<T>, A>[]
): ElementAction<T, A, ElemR<A>> =>
TODO()
type MatchRestr<R, A> =
A extends Action<any, infer AR>
? R extends Pick<AR, Extract<keyof AR, keyof R>> ? 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:
const wrapper = <E extends ElementAction<any, any>>(children: E[]) =>
h('div', children)
This raises a type-error:
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:
<E extends ElementAction<any, any>>(children: MatchRestr<TagR<any>, E>[]) => ...
And it indeed works, however as soon as I try to add something different it will break again:
const wrapper = <E extends ElementAction<any, any>>(children: MatchRestr<TagR<any>, E>[]) =>
h('input', [
onInput(TODO),
...children
])
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:
const wrapper = <E extends ElementAction<any, any>>(children: MatchRestr<TagR<any>, E>[]) =>
h<'input', OnInputAction|E>('input', [
onInput(TODO),
...children
])
Here is it in the Playground
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
}
};
@husnain129 not a correct assumption, Auth<"name" | "admin">("admin", "hello")
is a legal call
@husnain129 not a correct assumption,
Auth<"name" | "admin">("admin", "hello")
is a legal call
@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?
Typo, should be
Auth<"user" | "admin">("admin", { name: "" });
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 } };
A 'workaround' would be to use the is
keyword.
Another simple illustration at this StackOverflow question:
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.
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:
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 reallyT extends number ? T : never
which is assignable toT extends number ? number : string
.Similarly, with substitution types, we have additional information that we can leverage, e.g.: