This is simply mind blowing technique. Thank you Matt for everything you are doing. Every single video of yours is like a piece of wizardry, a potion that makes me a superhero
This is crazy i just had this problem and solved it with function overloads generics and some helper types, googled an eternity how to do this better without results and a few days later this just pops up in my feed. Worked first try, youre a legend!
I love this video format. Short video, with bits of knowlade. I've subscribed hoping I will be able to learn more TS with you, one small bit at a time :)
I just don't get why we need to use Extract. will not that result the same as Event? I think this also does the job: function sendEvent( ...args: TEvent extends {payload: infer TPayload} ? [type: TEvent["type], payload: TPayload]: [type: TEvent["type"]] ) Or am I missing something?
Based on this video, I created code that specifically changes the return type. I have the following union type and wanted to narrow down the type of the returned array depending on whether it's 'title' or 'body'. export type IndexMap = | { type: 'title'; keyword: string; target: Title } | { type: 'body'; keyword: string; target: Body } Therefore, I defined the return type as follows: function searchAt( content: string ): Extract['target'][] { return [] } If the type is 'title', it returns a Title[] and if it is 'body', it returns a Body[]. searchAt('.....') returns Title[] searchAt('....') returns Body[]
Great video Matt! A question a bit related: Is there a way to show some suggested autocompletions before some others? For example, a Button component properly typed receives some custom props (e.g. size, color, shape...) but also all the normal HTMLButton attributes (like type, value, etc...). The problem I faced is that when I trigger autocomplete for props it shows me all the props in alphabetic order, which is a bummer since I'd like to see my custom props/attributes first and then all the HTMLButton ones because otherwise they get kinda lost. Thanks :)
I have a situation similar to yours, but my "Event" type could be of 5 different types. What should I do? I've tried some approaches like using `payload as Interface1` or `payload as Interface2` before passing the parameters to the function. I tried using UnionTypes too, but it does not work because the payload could be Interface1 | Interface2 | Interface3 ..... Should i write a TypeGuard for each one?
You could use an Interface to hold different types. Like this: interface Event { login: [username: string, password: number], logout: [username: string], signin: [username: string, password: string, mail: string], } function sendEvent(type: Type, ...args: Event[Type]) {
} if you only have one argument, you can use it directly as the type in the interface while droppong the varargs. In this however vscode won't infer the type of the arg, even if it knows the value of type, while it does infer with the varargs. The calling behavior is the same in both situations.
Args variable inside function is of type `(parameter) args: [type: Type] | [type: Type, payload: unknown]`. Why doest TS infer the type of payload as `unknown`?
Very cool, very interesting! My question would be: what is the advantace of this syntax over passing an object in the form of: type Event = { type: "LOG_IN" payload: any } | { type: "SIGN_OUT" } function sendEvent(event: Event): void { return } sendEvent({ type: "LOG_IN", payload: "" }) sendEvent({ type: "SIGN_OUT" }) in this case, TypeScript would not let me append any payload on a function calling SIGN_OUT promting me that 'payload' does not exist in type '{ type: "SIGN_OUT"; }'. Or is there something to this that I'm missing completely?
Why this one does not seem to work? It is pretty similar: const sendEvent = ( ...args: Extract extends {payload: infer TPayl} ? [type: T["type"], payload: TPayl] : [type: T["type"]] ) => {};
This is really nice and elegant for the consumer, however there is a major problem with how you have set this up, which you didn't run into since you didn't actually write any code inside the function. The problem is that TypeScript always thinks the payload is of the `unknown` type, even after type guarding the first argument. I tried a different approach but still didn't get it to work properly. I have solved this in the past, though I'd have to look up how I did it, it's been a couple years. It certainly wasn't as elegant as this.
Yes, inside generic functions you're going to need to do some casting. TS just isn't clever enough to figure out their internals. I generally make sure that everything works for the consumer, then cast a bunch inside the function and make sure it's tested.
@@mattpocockuk Thank you for the response, imagine I have interface Events { "ready": [... parameters] //etc... } If there was a better way to communicate I could give a proper example with links! Thank you!
I don’t know how there is nobody freaking out with these monsters. Building such an inline hell just to type a language. Honestly just considered a compiled language
I was able to access the arguments by changing the union type and then casting args[1]. This kinda kills the whole point of it being generic though ,so I would also love to see how @mattpocockuk would answer your question...: type LogIn = { type: "LOG_IN"; payload: { userId: string; }; } type SignOut = { type: "SIGN_OUT"; }; export type Event = LogIn | SignOut const sendEvent = ( ...args: Extract extends { payload: infer TPayload } ? [type: Type, payload: TPayload] : [type: Type] ) => { const eventType = args[0]; const userId = args[1] as LogIn["payload"] if (userId) { ... } };
You can separate this Extract part as a type helper and then use it to cast types inside the function: type EventArgs = Extract extends { payload: infer TPayload } ? [payload: TPayload] : []; const emit = (eventType: T, ...args: EventArgs) => { switch (eventType) { case 'SIGN_IN': const [payload] = args as EventArgs; } };
The only downside I see on this approach is that It loses conciseness since before we were receiving an eventType and an optional payload that might be null or an object, but at javascript runtime level we are converting this args into an array and later need to destructure it const [type, payload = undefined] = args
Actually, none of this is necessary. Just pass 1 argument of type Event, and the type system will support it. For instance, in the branch where arg.type === "SIGN_OUT", there will be no payload. Problem solved.
This is enough to make a grown man cry.
It really is...
Advanced TS FTW yo!
It's not that difficult, to be honest.
@@dheerajsinghnagdaliits ridiculously difficult for such simple thing)
@@MeLuCk3R I'm looking for a partner to practice Typescript technical interviews if you're interested, lemme know. I agree btw.
I spent many hours a while back trying to figure this one out! I have at least four places in my current codebase that could use this. Great video!
This is simply mind blowing technique.
Thank you Matt for everything you are doing. Every single video of yours is like a piece of wizardry, a potion that makes me a superhero
This is great, been experimenting with generic void types as optional args, failed miserably haha
My gosh, it's exactly what I was trying to achieve recently and failed! This is super useful. Thank you, man!
This is crazy i just had this problem and solved it with function overloads generics and some helper types, googled an eternity how to do this better without results and a few days later this just pops up in my feed. Worked first try, youre a legend!
I love this-thank you
Very useful . I will share with my friend
I love this video format. Short video, with bits of knowlade. I've subscribed hoping I will be able to learn more TS with you, one small bit at a time :)
Those 2 minutes videos takes me like to 2 days to partially absorb all those wizardry
You deserve way more subscribers
Pure gold thx
I just don't get why we need to use Extract. will not that result the same as Event? I think this also does the job:
function sendEvent(
...args: TEvent extends {payload: infer TPayload} ? [type: TEvent["type], payload: TPayload]: [type: TEvent["type"]]
)
Or am I missing something?
You're a true wizard
Based on this video, I created code that specifically changes the return type. I have the following union type and wanted to narrow down the type of the returned array depending on whether it's 'title' or 'body'.
export type IndexMap =
| { type: 'title'; keyword: string; target: Title }
| { type: 'body'; keyword: string; target: Body }
Therefore, I defined the return type as follows:
function searchAt(
content: string
): Extract['target'][] {
return []
}
If the type is 'title', it returns a Title[] and if it is 'body', it returns a Body[].
searchAt('.....') returns Title[]
searchAt('....') returns Body[]
That's wild.
thanks
This one is quite complex
I subscribed please teach me your voodoo. Self taught in the industry now for 5 years transitioned to backend 2 years ago. So much to learn.
Very cool
Great video Matt!
A question a bit related: Is there a way to show some suggested autocompletions before some others? For example, a Button component properly typed receives some custom props (e.g. size, color, shape...) but also all the normal HTMLButton attributes (like type, value, etc...). The problem I faced is that when I trigger autocomplete for props it shows me all the props in alphabetic order, which is a bummer since I'd like to see my custom props/attributes first and then all the HTMLButton ones because otherwise they get kinda lost. Thanks :)
I don't think so - you'd need to build a VSCode extension for that sort of info.
I have a situation similar to yours, but my "Event" type could be of 5 different types. What should I do? I've tried some approaches like using `payload as Interface1` or `payload as Interface2` before passing the parameters to the function. I tried using UnionTypes too, but it does not work because the payload could be Interface1 | Interface2 | Interface3 ..... Should i write a TypeGuard for each one?
You could use an Interface to hold different types.
Like this:
interface Event {
login: [username: string, password: number],
logout: [username: string],
signin: [username: string, password: string, mail: string],
}
function sendEvent(type: Type, ...args: Event[Type]) {
}
if you only have one argument, you can use it directly as the type in the interface while droppong the varargs.
In this however vscode won't infer the type of the arg, even if it knows the value of type, while it does infer with the varargs.
The calling behavior is the same in both situations.
Args variable inside function is of type `(parameter) args: [type: Type] | [type: Type, payload: unknown]`. Why doest TS infer the type of payload as `unknown`?
Setting the scalability aside for a moment, would the result be the same if we used method overload for both events?
I don't understand questions like this. You're on the internet. Write a little code and see what happens
Very cool, very interesting!
My question would be: what is the advantace of this syntax over passing an object in the form of:
type Event = {
type: "LOG_IN"
payload: any
} |
{ type: "SIGN_OUT" }
function sendEvent(event: Event): void {
return
}
sendEvent({ type: "LOG_IN", payload: "" })
sendEvent({ type: "SIGN_OUT" })
in this case, TypeScript would not let me append any payload on a function calling SIGN_OUT promting me that 'payload' does not exist in type '{ type: "SIGN_OUT"; }'.
Or is there something to this that I'm missing completely?
Absolutely - but I wanted to show how, if you wanted to, you could describe dynamic function arguments.
Why this one does not seem to work? It is pretty similar:
const sendEvent = (
...args: Extract extends {payload: infer TPayl}
? [type: T["type"], payload: TPayl]
: [type: T["type"]]
) => {};
Madness.
Did you create typescript?
This is really nice and elegant for the consumer, however there is a major problem with how you have set this up, which you didn't run into since you didn't actually write any code inside the function. The problem is that TypeScript always thinks the payload is of the `unknown` type, even after type guarding the first argument. I tried a different approach but still didn't get it to work properly. I have solved this in the past, though I'd have to look up how I did it, it's been a couple years. It certainly wasn't as elegant as this.
Yes, inside generic functions you're going to need to do some casting. TS just isn't clever enough to figure out their internals. I generally make sure that everything works for the consumer, then cast a bunch inside the function and make sure it's tested.
@@mattpocockuk if possible, I want to watch casting examples, as extending this good sample movie.
How would I go about doing this with a interface?
Amazing Video by the way!
How do you mean?
@@mattpocockuk Thank you for the response, imagine I have
interface Events {
"ready": [... parameters]
//etc...
}
If there was a better way to communicate I could give a proper example with links! Thank you!
@@anishshobith9707 If you send a TS playground that should give me a better idea.
Gose over my head 😂😮
Ohh noo this one looks really bad to maintain, what if you have multiple args
You can have multiple generic arguments
I don’t know how there is nobody freaking out with these monsters. Building such an inline hell just to type a language. Honestly just considered a compiled language
Doesn’t this make code much harder to read and understand if you are not super familiar with Typescripts internals and quirks
This isn't TS internals, it's all documented API's. If you want, you can link them to this video in the comments!
INSANITY!!
How to get args type inside function?
const sendEvent2 = (
...args: Extract extends { payload: infer TPayload }
? [type: Type, payload: TPayload]
: [type: Type]
) => {
const eventType = args[0];
const payload = args[1]
};
eventType has proper type but payload type is unknown
I was able to access the arguments by changing the union type and then casting args[1]. This kinda kills the whole point of it being generic though ,so I would also love to see how @mattpocockuk would answer your question...:
type LogIn = {
type: "LOG_IN";
payload: {
userId: string;
};
}
type SignOut = {
type: "SIGN_OUT";
};
export type Event = LogIn | SignOut
const sendEvent = (
...args: Extract extends { payload: infer TPayload }
? [type: Type, payload: TPayload]
: [type: Type]
) => {
const eventType = args[0];
const userId = args[1] as LogIn["payload"]
if (userId) {
...
}
};
You can separate this Extract part as a type helper and then use it to cast types inside the function:
type EventArgs =
Extract extends { payload: infer TPayload }
? [payload: TPayload]
: [];
const emit = (eventType: T, ...args: EventArgs) => {
switch (eventType) {
case 'SIGN_IN':
const [payload] = args as EventArgs;
}
};
🤯🤯
Why not just have separate functions?
Let's imagine you want to add TS types to a JS function that looks like this. Having separate functions wouldn't work for that case.
wheres the playground
jk great video!
The only downside I see on this approach is that It loses conciseness since before we were receiving an eventType and an optional payload that might be null or an object, but at javascript runtime level we are converting this args into an array and later need to destructure it
const [type, payload = undefined] = args
const sendEvent = (
type:T, payload?: Extract extends {payload: infer U} ? U : never
) =>{
console.log(type, payload);
}
sendEvent("LOG_IN", {userId:"test"})
sendEvent("SIGN_OUT")
Actually, none of this is necessary. Just pass 1 argument of type Event, and the type system will support it. For instance, in the branch where arg.type === "SIGN_OUT", there will be no payload. Problem solved.
Of course, you can design an API where this is not necessary. But what about typing existing API's where this is an established behaviour?
thats witchcraft lol
in cpp there is method overloading but in ts 🥲 thats horrible