You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
return type of function being union instead of one-of-union #55089
import{assert,Equals}from'tsafe'// Example usage with FooBar typetypeFoo={foo: string;};typeBar={bar: number;};typeFooBar={foo: Foo;bar: Bar;};typeKinded<T>={[KinkeyofT]: [key: K,val: T[K]]}[keyofT]functiontest<T>(...[key,val]: Kinded<T>){returnval};// @ts-expect-error -> ts complains expectedly that args[1] should be Fooconsterror2=test<FooBar>('foo',{bar: 0});// @ts-expect-error -> ts complains expectedly that args[1] should Barconsterror1=test<FooBar>('bar',{foo: 'string'});// from the errors above we know args[1] is properly typed// but the returned value is still `Foo | Bar` :\constfoo=test<FooBar>('foo',{foo: 'string'});constbar=test<FooBar>('bar',{bar: 0});// ---cut---assert<Equals<typeoffoo,Foo>>()assert<Equals<typeofbar,Bar>>()
🙁 Actual behavior
Firstly, I do understand that the function defines arguments as (basically) declare function test<T>(...args: [keyof T, T[keyof T]]), and so the compiler thinks "test() returns args[1] which is T[keyof T] so the returned type should be T[keyof T]" (which in my example is Foo | Bar).
Where I think this stops to be logical is that type inference based on args[0] actually works great for function arguments: it successfully narrows the type of args[1] due to the discriminating union created by Kinded. We can see that this happens well thanks to the error1 and error2 constants.
🙂 Expected behavior
So, since the function only contains return val (args[1] of the tuple), and since the type of val was properly inferred in the function arguments, i guess it should be expected that return type of the function should be properly inferred rather than the static union of.
As a workaround, there's possibility to curry the function, which would end up as a workaround for partial type inference. Posting here so that people interested can find a temporary solution.
What happens in Kinded is opaque to what happens in your test function, also notice that in test you are simply not using the value of key, indeed, if I've got your intent correctly, I think you are just not using the right parameters. Here is a piece of code (with some renaming trying to make intents more explicit) that seems to do the job, with two major modifications re your code: 1) the type parameter I am actually using, tied to a specific key, not to the whole "type map", and 2) I am not using spread arguments, which complicates things significantly: returning an array of results just brings us back to type unions, maybe returning an object of type Partial<TypeMap> would do the trick, but I'll leave solving this as a next step and only if you confirm and clarify the requirement, otherwise I simply don't see the point, or maybe it shouldn't be called call test (which maybe is not the right name anyway, this is more of a checked coercion)...:
typeFoo={foo: string;};typeBar={bar: number;};typeTypeMap={foo: Foo;bar: Bar;};functiontest<KextendskeyofTypeMap>(key: K,val: TypeMap[typeofkey]): TypeMap[K]{returnval;};// @ts-expect-error : Argument of type '{ bar: number; }'// is not assignable to parameter of type 'Foo'consterrorFoo=test('foo',{bar: 0});// @ts-expect-error : Argument of type '{ foo: string; }'// is not assignable to parameter of type 'Bar'consterrorBar=test('bar',{foo: 'string'});// the returned value here is `Foo` as expectedconstfoo=test('foo',{foo: 'string'});// the returned value here is `Bar` as expectedconstbar=test('bar',{bar: 0});
there's something not on par with my actual issue in your proposal, is that TypeMap is hardcoded into test. What i was looking for is to do exactly the same but with a "pluggable" type map.
you can see here:
consterror2=test<FooBar>('foo',{bar: 0});
that i'm purposely passing the type into the function's generic. The intent behind is that this function should be reusable for anything extending Record<string, any>.
I have played with it a little bit more and below is the best I could get (so far), still there is (IME) something "smelly" with this pattern, anyway maybe you/others can take it from here.
typeFoo={foo: string;};typeBar={bar: number;};typeKeyed<M>=Record<keyofM,M[keyofM]>;typeTypeMap={foo: Foo;bar: Bar;};// without the default for param `T`, cannot be called with// only one type argument instead of getting a union:functiontest<MextendsKeyed<M>,TextendsM[keyofM]=M[keyofM]>(val: T): T{returnval;};// @ts-expect-error : Argument of type '{ bar: number; }'// is not assignable to parameter of type 'never'consterrorFoo=test({bar: 0});// @ts-expect-error : Argument of type '{ foo: string; }'// is not assignable to parameter of type 'never'consterrorBar=test({foo: 'string'});// returns the union `Foo | Bar`constunionFoo=test<TypeMap>({foo: 'string'});// returns the union `Foo | Bar`constunionBar=test<TypeMap>({bar: 0});// returns exactly `Foo` (but, isn't this just a// convoluted way to say "I want Foo" to begin with?)constfoo=test<TypeMap,Foo>({foo: 'string'});// returns exactly `Bar` (but, isn't this just a// convoluted way to say "I want Bar" to begin with?)constbar=test<TypeMap,Bar>({bar: 0});
This improves on my previous code and might even be it (up to you to say), surely it's the nearest I can get that still makes sense to me. I have shuffled things around a bit, so if one wants it's still parametrizable on a "type map", though at a quite different level here:
functioncheck<T=never,UextendsT=T>(val: U): U{returnval;};typeFoo={foo: string;};typeBar={bar: number;};typeTypeMap={foo: Foo;bar: Bar;};// @ts-expect-error : Argument of type '{ bar: number; }'// is not assignable to parameter of type 'never'consterrorFoo=check({bar: 0});// @ts-expect-error : Argument of type '{ foo: string; }'// is not assignable to parameter of type 'never'consterrorBar=check({foo: ''});// returns `Foo` as expectedconstfoo=check<TypeMap['foo']>({foo: ''});// returns `Bar` as expectedconstbar=check<TypeMap['bar']>({bar: 0});// returns `Foo` as expected// (indeed, `TypeMap['foo']` is equivalent to just `Foo`)constfoo2=check<Foo>({foo: ''});// returns `Bar` as expected// (indeed, `TypeMap['bar']` is equivalent to just `Bar`)constbar2=check<Bar>({bar: 0});
P.S. Keeping in mind that a "checked coercion" is just a simple example of a generic function of the kind I understand you are asking for, but is per se quite pointless, since a const foo: Foo = something or indeed a const foo: TypeMap['foo'] = something already achieve that result.
I don't understand what the alleged defect is. You have type arguments in all these positions, so of course f<T>(a) and f<T>(b) aren't going to return different types -- you've fixed them both to T
If I’m reading this right, OP seems, in general, to expect <T, U>(x: T | U) => x to return T or U only (instead of T | U) depending on which was passed in, but that’s not how return-type inference works
I do know that <T, U>(x: T | U) => x will return T | U but here I think that the case is different since it's not a plain union but rather a discriminated union based on one of the argument of the function.
The function can be seen as f<T, K extends keyof T>(a: K, b: T[K]) => b, but in order to avoid having to pass manually K, I built the function's arguments as a tuple which acts as a discriminated union like this:
type Kinded<T> = {
[K in keyof T]: [key: K, val: T[K]]
}[keyof T]
...which allows to change f<T, K extends keyof T>(a: K, b: T[K]) => b to become f<T>(...args: Kinded<T>) => args[1]with the expectation that, due to the discriminated union built for args, then args[1] should not be T[keyof T] but a solid T[K].
I also do understand that, without args[0] the computed returned type is Kinded<T>[1] which is indeed a union of all T[K] but how come that at use of the function, the returned type is not inferred properly?
on the side i've written in the ticket description that i do understand what you tell i don't.
Firstly, I do understand that the function defines arguments as (basically) declare function test(...args: [keyof T, T[keyof T]]), and so the compiler thinks "test() returns args[1] which is T[keyof T] so the returned type should be T[keyof T]" (which in my example is Foo | Bar).
where i stopped understanding this is when a discriminating union comes into play.
Activity
y-nk commentedon Jul 20, 2023
As a workaround, there's possibility to curry the function, which would end up as a workaround for partial type inference. Posting here so that people interested can find a temporary solution.
instead of expected:
you'll have to add
()(...for no other reason than a TS one)jp-diegidio commentedon Jul 20, 2023
What happens in
Kindedis opaque to what happens in yourtestfunction, also notice that intestyou are simply not using the value ofkey, indeed, if I've got your intent correctly, I think you are just not using the right parameters. Here is a piece of code (with some renaming trying to make intents more explicit) that seems to do the job, with two major modifications re your code: 1) the type parameter I am actually using, tied to a specific key, not to the whole "type map", and 2) I am not using spread arguments, which complicates things significantly: returning an array of results just brings us back to type unions, maybe returning an object of typePartial<TypeMap>would do the trick, but I'll leave solving this as a next step and only if you confirm and clarify the requirement, otherwise I simply don't see the point, or maybe it shouldn't be called calltest(which maybe is not the right name anyway, this is more of a checked coercion)...:Link to code in playground
y-nk commentedon Jul 20, 2023
Julio Di Egidio (@jp-diegidio) thanks for taking the time to look at my problem 🙏
there's something not on par with my actual issue in your proposal, is that
TypeMapis hardcoded intotest. What i was looking for is to do exactly the same but with a "pluggable" type map.you can see here:
that i'm purposely passing the type into the function's generic. The intent behind is that this function should be reusable for anything extending
Record<string, any>.jp-diegidio commentedon Jul 20, 2023
Julien Barbay (@y-nk) You are welcome and thank you too, I myself am learning TS...
I have played with it a little bit more and below is the best I could get (so far), still there is (IME) something "smelly" with this pattern, anyway maybe you/others can take it from here.
Link to code in playground
jp-diegidio commentedon Jul 20, 2023
This improves on my previous code and might even be it (up to you to say), surely it's the nearest I can get that still makes sense to me. I have shuffled things around a bit, so if one wants it's still parametrizable on a "type map", though at a quite different level here:
Link to code in playground
jp-diegidio commentedon Jul 20, 2023
P.S. Keeping in mind that a "checked coercion" is just a simple example of a generic function of the kind I understand you are asking for, but is per se quite pointless, since a
const foo: Foo = somethingor indeed aconst foo: TypeMap['foo'] = somethingalready achieve that result.RyanCavanaugh commentedon Jul 20, 2023
I don't understand what the alleged defect is. You have type arguments in all these positions, so of course
f<T>(a)andf<T>(b)aren't going to return different types -- you've fixed them both toTfatcerberus commentedon Jul 20, 2023
If I’m reading this right, OP seems, in general, to expect
<T, U>(x: T | U) => xto returnTorUonly (instead ofT | U) depending on which was passed in, but that’s not how return-type inference worksy-nk commentedon Jul 26, 2023
Ryan Cavanaugh (@RyanCavanaugh) Bruce Pascoe (@fatcerberus) yes and no.
I do know that
<T, U>(x: T | U) => xwill returnT | Ubut here I think that the case is different since it's not a plain union but rather a discriminated union based on one of the argument of the function.The function can be seen as
f<T, K extends keyof T>(a: K, b: T[K]) => b, but in order to avoid having to pass manuallyK, I built the function's arguments as a tuple which acts as a discriminated union like this:...which allows to change
f<T, K extends keyof T>(a: K, b: T[K]) => bto becomef<T>(...args: Kinded<T>) => args[1]with the expectation that, due to the discriminated union built forargs, thenargs[1]should not beT[keyof T]but a solidT[K].I also do understand that, without
args[0]the computed returned type isKinded<T>[1]which is indeed a union of allT[K]but how come that at use of the function, the returned type is not inferred properly?y-nk commentedon Jul 26, 2023
on the side i've written in the ticket description that i do understand what you tell i don't.
where i stopped understanding this is when a discriminating union comes into play.
RyanCavanaugh commentedon Jul 26, 2023
Please don't post screenshots of code.
It seems like this is more of a question than a bug report; nothing outlined here is indicative of a defect.