-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comparing constrained generic types/substitution types to conditional types #23132
Comments
@mhegazy [1] Counter example: pick |
That is the type that results narrowing the type of |
Sorry I misunderstood. I thought that 'behaving as As in the top case the constraint applies to the type |
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 |
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: 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 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 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, " |
@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 |
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. 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 |
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}
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 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 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
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 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:
^ 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: const wrapper = <E extends ElementAction<any, any>>(children: MatchRestr<TagR<any>, E>[]) =>
h('input', [
onInput(TODO),
...children
])
Not sure why it infers as 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.
|
@husnain129 not a correct assumption, |
@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: "" }); |
A 'workaround' would be to use the |
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. |
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.:
The text was updated successfully, but these errors were encountered: