-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Use conditional type to narrow return type of function with a union-typed argument #24929
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
More features along these lines are really important to me, too. |
Duplicate of #22735 it seems. Nonetheless, I agree strongly with OP |
Would love to see this supported. Until then, there are two workarounds you can use: 1. use an
|
@rozzzly That is quite helpful, thank you. |
Note that function overloads are not particularly better here, for typesafety. There is absolutely no checking that your overloads are accurate: only that they could be accurate, based on the primary type signature. Conditionally typing the primary signature might help with that, haven't tested it that thoroughly. Also note that, at least in my experience, which overload Typescript will choose can be very surprising and (apparently, from the user's perspective) inconsistent. Due to these issues, our project has strongly encouraged developers to avoid using overloads at all. They are occasionally the least-bad option, as they may be here, but they are rarely ever a good option. They almost-always are a work-around for some limitation in the type system, but it's not always obvious that's what they are, which means casting may be considered superior (because at least it's obvious and honest about what it's doing). |
Duplicate #22735 The core problem here is that type guard operate on values (not type parameters), and you can't actually make meaningful proofs about type parameters because a) they're not manifest at runtime so anything you do is really quite suspect and b) there might be more than one type guard active against expressions matching a type parameter at a time anyway! For example: function fn<T extends string | number | boolean>(x1: T, x2: T): T extends string ? "s" : T extends number ? "n" : "b" {
if (typeof x1 === 'string' && typeof x2 === 'number') {
// ... is this OK?
return 's';
}
} |
That wouldn't be OK, because There are a lot of limitations like this where TS doesn't do anything because it cannot make guarantees in the general case, but there are a lot of interesting and useful special cases where guarantees would be possible. Another one that springs to mind immediately is TS not recognizing when unions are mutually exclusive and leveraging that for making guarantees it could not if the unions were not mutually exclusive—note that this doesn't really have anything to do with the |
Here is another example that fails, in the context of code for a project of mine: I believe this is the same issue at play. (aside: not sure why the typescript playground is ignoring the undefined type I use in it lol. but the point stands) |
I expected this example to work, but it didn't:
Instead, there are two errors: Works as expected if cast the strings to any. |
@krryan
@lukeautry type Options = 'yes' | 'no';
const op = <T extends Options>(value: T): T extends 'yes' ? 'good' : 'bad' => {
if (value === 'yes') return 'good';
if (value === 'no') return 'bad';
return value; // Would need to have value narrow to never, which it does not
}; For that to work the compiler really needs to know that type Options = 'yes' | 'no';
function op(value: 'yes'): 'good';
function op(value: 'no'): 'bad';
function op(value: Options): 'good' | 'bad' {
if (value === 'yes') return 'good';
if (value === 'no') return 'bad';
return value; // never
} The complications that Ryan mentions mean having a general solution that works beyond very simple cases is non-trivial. |
This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes. |
Should @typescript-bot be closing a duplicate when the older issue is locked? That doesn't seem healthy. |
Any updates on this? I think we should reopen this, as the other issue got closed (#22735) and this issue is better described. This is kind of a common use case if you make use of function returning either (I am just writing so that the issues does not go stale) |
Support for this would make conditional types far more valuable. Is this even on the roadmap? |
I thought something like this might work but ... type TestFuncRes = {
type1: number;
type2: string;
};
type Test<T extends keyof TestFuncRes> = {
(arg: T): TestFuncRes[T];
};
const testFunc = (arg: keyof TestFuncRes) => {
const type1: number = 22;
const type2: string = 'test string';
return arg === "type1" ? type1: type2
};
const test: Test<'type1'> = testFunc('type1'); any thoughts? |
I have a similar issue to the comment above where I have a helper function, that checks your on the correct product for a feature as well as checking if you have the correct permissions.
So you can see that it can be used as a function or as a wrapper for a component, this is so we can disable things your allowed to see but not edit and also hide things that aren't features of the product you are on. My problem is that when using it as a component wrapper
It throws an error at both the component usage and the function usage Function usage = I want to be able to change the return type definition dependent on whether |
I have a very similar use case const propOrValue = <
T extends Record<string, any>,
K extends keyof T
>(
obj: T,
key?: K,
): K extends undefined ? T : T[K] => {
if (key) {
return obj[key];
}
return obj; // <- error here
}; the possible workaround is use return obj as any; Update: Here is explanation #22735 (comment) |
@jack-williams the example you gave is not ideal/sound 👇
Ideally, when I change |
Could this be revisited? type MappingOf<T extends 'X' | 'Y'> =
T extends 'X' ? '1'
: T extends 'Y' ? '2'
: never
function mappingOf<T extends 'X' | 'Y'>(t : T): MappingOf<T> {
switch (t) {
case 'X': return '1'
// Type '"1"' is not assignable to type 'MappingOf<T>'.ts(2322)
case 'Y': return '2'
// Type '"2"' is not assignable to type 'MappingOf<T>'.ts(2322)
}
} I'd like to express a mapping between two values in types ( |
What's the state of this? Is this being tracked by some other issue? Seems like a pretty common js pattern, but can't find this problem tracked in any open issue? |
Looks like it's dead. Too bad. The best thing about this is that ... wait for it ... the TypeScript docs actually give a function signature with a generic conditional return type as an example, but they smartly leave out the implementation ... is it because it cannot really be implemented in current TypeScript, or what? Why show that off as a feature, when you don't provide a working implementation? :D None of the workarounds we have so far are type-safe, except for having multiple functions (with different names), but that's not really DRY. This is one of the cases where logically, you'd think something would just work, but in reality it does not. I originally found this thread trying to figure out how I could get rid of function overloads, because they seem to deal with this, but it's at the expense of the function implementation not really being type-safe, and I don't like that. (TypeScript doesn't really make sure that the function implementation adheres to ALL its overloads). Using a generic conditional was my second bet, but sadly ... it also is unusable. Makes one wonder why the syntax is even allowed for function return types :D |
Does the following example also fall into this category? type In1 = 'a' | 'b';
type Out1<T extends In1> = T extends 'a'
? {type: 'a'; a: true}
: T extends 'b'
? {type: 'b'; b: true}
: never;
function func1<T extends In1>(type: T): Out1<T> {
switch (type) {
case 'a':
// Type '{ type: T; a: true; }' is not assignable to type 'Out1<T>'
return {type, a: true};
case 'b':
// Type '{ type: T; b: true; }' is not assignable to type 'Out1<T>'
return {type, b: true};
default:
unreachable(type);
}
} It is a pattern continuously used throughout our project and I can't see anything wrong with it. Mapping like this seems like a fundamental feature of TypeScript to me, so I was confused that it didn't work. Can we reopen this or is that particular problem tracked in another issue? |
The general idea of narrowing on generics is tracked at #33014 |
Can we reopen this though? I lost like 2 hours on this issue going back and reading the docs because I thought I must be missing something (would a PR adding a note in the docs get merged?). |
I find function overloads to be a cleaner and easier to read approach to specifying conditional return types. |
@whschultz Function overloads provide the absolute bare minimum of type safety: they only check that the overloaded types could be assigned to the “real” signature, not that the underlying code actually does what it claims with respect to them. The hope for conditional types was that they would provide a greater guarantee within the body of the function, but because of these issues and the need to cast them, they don’t. |
this causes another problem, TS will not report wrong return values
it allowed returning an object which is a non-expecting
also, this one causes a problem when you need to return a different type than
|
any other ways for conditional return type except overloads, here is function implementation signature
the |
Search Terms
conditional return type narrowing generics
Suggestion
It seems like it should be possible to constrain the return type of a function using a conditional expression:
For example,
Use Cases
A common use case is a function which can process single objects or arrays of objects, and returns single objects or an array, respectively.
Examples
Checklist
My suggestion meets these guidelines:
The text was updated successfully, but these errors were encountered: