-
Notifications
You must be signed in to change notification settings - Fork 227
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
Comments: Conditional types in TypeScript #500
Comments
Hands down the best thing I've read on conditional types. Well done! The TypeScript should make this part of their official docs! cc @DanielRosenwasser |
I have been struggling with understanding this topic almost every day for months. This is the first thing I've read in plain speech—where I could really understand it. Your writing is excellent, entertaining, and informative. This should absolutely be a part of the TypeScript documentation. Thank you! |
Thanks for the kind words, peeps 😊 I'm super glad you've found it helpful! 💃 |
@ds300 How would you implement a body of
fails with
|
Ah yeah! That's an open issue microsoft/TypeScript#24929 — I left this out of my talk but should probably have included it in the blog post as another caveat. Unfortunately the only workaround right now is to cast your return values as |
With your first code snippet, I get the following error
This is the link to the playground. I'm probably missing something simple. Any ideas? Did something change recently? I'm using TS 3.2.2 |
Hey @bdurrani! 👋 You're right. That's an open issue: TS can't type-check the return value of functions with conditional return types defined in terms of type parameters. See the link in the comment above yours for more info ☝️ |
I should have look at that. Thanks for responding. great article. |
This is such a fantastic post. Thank you so much for writing it! |
Thanks for the fantastic post! I'm wondering if this is similar to @bdurrani's issue, but I'm having trouble finding a way to write a typesafe implementation of a function dispatch<T extends ActionType>(
type: T,
args: ExtractActionParameters<Action, T>
): void {
if (type === 'LOG_IN') {
console.log(args.emailAddress) // error
}
} Inside of the The best thing I've come up with so far is this (fairly ugly) type-guard: function isType<T extends ActionType>(
desired: T,
actual: ActionType,
args: ExcludeTypeField<Action>
): args is ExtractActionParameters<Action, T> {
return desired === actual;
}
function dispatch<T extends ActionType>(
type: T,
args: ExtractActionParameters<Action, T>
): void {
if (isType("LOG_IN", type, args)) {
console.log(args.emailAddress); // 👍
}
} Have others run into this, and come up with a way to make the TypeScript compiler be a little more intuitive without resorting to type-guards or unsafe casts? |
I suppose the problem is that TypeScript can't yet narrow the types of two independent variables, even if their types are codependent. One thing I might be tempted to try would be combining the values back into a single function dispatch<T extends ActionType>(
type: T,
args: ExtractActionParameters<Action, T>
): void {
const action = {type, ...args} as Action
switch (action.type) {
case "LOG_IN":
action.emailAddress // 👍
break;
}
} Obviously not ideal if you need to run |
Thank you for the post. It is extremely helpful. Could you please advise how I can address the issue below? const myFunc = [
{ func: (s: string): number => parseInt(s) },
{ func: (n: number): boolean => n === 17 }
];
type FuncType = (typeof myFunc extends Array<(infer A)> ? A : never)["func"];
type ExtractInput<T> = T extends (i: infer I) => any ? I: never;
type ArgsType = ExtractInput<FuncType>;
const executeFunc = (f: FuncType, input: ArgsType) => f(input); compiier doesn't like See it Playground |
@hienqnguyen Yeah I see what's going on there. That's the correct behaviour. Let me break it down:
type FuncType = ((s: string) => number) | ((n: number) => boolean) So when you type ArgsType = ExtractInput<(s: string) => number> | ExtractInput<(n: number) => boolean> and so type ArgsType = string | number But if you try to call I think a way to fix this would be to avoid making A hacky way to do that which just popped into my head would be to wrap both sides of the type ExtractInput<T> = Array<T> extends Array<(i: infer I) => any> ? I: never; You could do it with a tuple for fewer characters, but even more confusing to read IMO (definitely add a comment if you decide to use either of these 😅) type ExtractInput<T> = [T] extends [(i: infer I) => any] ? I: never; |
No, we cannot use conditional types here since the signature is invalid. It's not a bug, it's a design limitation so it will probably never work.
types and classes are different things: a type refers to interface and a class refers to implementation.
A extends B is a lot like 'A is a subset of B' |
Hi @marzelin 👋 Thanks for the feedback! I will address your concerns individually
The type signature is legal, you can try it out :) The problem you highlight might be the one already discussed earlier in this thread, i.e. that TypeScript can't safely check the return values of a function with a conditional return type.
If by 'type' you mean 'TypeScript type' then yeah 👍 I specifically mentioned 'runtime' type checking though, so hopefully people picked up on the fact that I wasn't only talking about TypeScript types in that particular sentence. Maybe it wasn't clear enough. Thanks for pointing it out!
I think I can see how this is confusing. I was implicitly using the mathematical notion of a 'set' here. A TS interface is a set of properties. In that regard, The confusion is likely coming from the fact that in type systems we use the terms 'subtype' and 'supertype' to refer to exactly the opposite kinds of inheritance relationships to 'subset' and 'superset' in maths 😅 Maybe I could have done more to highlight this distinction! Thanks again, this was useful feedback. |
Great explanations, examples and expressivity :) 🙏🙏🙏 |
Has anyone tried this approach yet? My compiler complains that the respective property does not exist on type Action (Which is to be expected as Action is an union type). Edit: This works: function dispatch<T extends ActionType>(
type: T,
args: ExtractActionParameters<Action, T>
): void {
switch (action.type) {
case "LOG_IN":
const action = {type, ...args} as {type: "LOG_IN", emailAddress: string};
action.emailAddress
break;
}
} |
Thank you for this article! I'm working in a Vue.js + Vuex app and types in Vuex are not the best, and the article helped me understand many concepts and ended up creating super strong types for I would recommend changing the name though. I found it because I was googling to solve a different problem (which I solved too haha) |
This is my new favorite article I've ever read, anywhere. |
As an intrepid reader, I've solved the exercise given at the end of the post 😅. Declaring type ExtractActionParameters<A, T> = A extends { type: T }
? {} extends Omit<A, 'type'>
? never
: Omit<A, 'type'>
: never; |
@vicke4 Awesome! A few people have sent me solutions to that challenge and I'm amazed that they've all been different! Yours is very neat 💯 |
@ds300 First of all, thank you so much for the blog post. It is helping a lot of users to understand the concept. I'd really love to see other solutions. If you don't mind, please share it in a gist. |
Simply amazing post! |
What a phenomenal post! Thank you. I'm new to TypeScript, and this really opened my eyes a lot. I looked further into the
|
I found a small issue with this solution. Calling dispatch like so:
Will give an unexpected error like: The real error should sound like: Is there a solution that fixes this? |
@IonelLupu yes, you can make the second argument for function dispatch<T extends ActionType>(
type: T,
args?: ExtractActionParameters<Action, T>
): void {
console.log(type, args)
} But that could be unsafe so look at the end of the post – https://artsy.github.io/blog/2018/11/21/conditional-types-in-typescript/ Also came here to thank the author for this incredible piece. It helped me with type inference from a stringified argument. |
Thanks for this article, this was really informative! Have you considered using a different style approach though? I guess it's roughly the same functionally just doesn't show off the conditional types.
And then you instead reference the EventMap's key -> value mapping, using the key as the identifier:
|
I still find myself coming back to this article months later. I would pay to subscribe to a newsletter of TypeScript articles like this if you’ve ever considered it. Has anyone come across a similar piece for TS 4.1’s new features? |
my solution for the exercise, which used spread operator to gather function arguments: |
Hello, const processStuff = <T extends string | null>(
text: T,
): T extends string ? string : null => text && text.toLowerCase(); // TS2322: Type 'string' is not assignable to type 'T extends string ? string : null'.
const t = processStuff("elo");
const n = processStuff(null);
but TS complains. What is wrong? I've tried that on TS 4.1.2 and 4.1.5 |
Hi @dzek69 this was discussed above in #500 (comment) Apologies for the confusion |
@ds300 Wow. It's been a while, I'm usually avoiding reading stuff from 2+ years ago, because it's usually out of date. Wow. Thanks for your comment. |
Thank you for a longgggggggggggg and great post |
Thank you for taking time and sharing your knowledge publicly for free. I am a beginning software developer attempting to learn typescript and am shocked how difficult certain parts are. I don't even understand 10% of the article but still can recognize that it is something of very good value (well I read through all comments=)). I really hope in 3-4 months I will get to a level where I will be able to understand at least 50% of what is written in the article. Thank you again; your time spent on this material is appreciated. |
http://artsy.github.io/blog/2018/11/21/conditional-types-in-typescript/
The text was updated successfully, but these errors were encountered: