I have an issue with TypeScript 4.9.4 (in case version matters).
I expect it should be possible to alter the return type of the function processFooBar
below based on the type of the function passed inside.
I need to preserve named inferred type as well.
The current best attempt is below. It infers types correctly, but only when type parameters are specified at the call site explicitly, which is not good for me:
import { expectAssignable, expectNotType, expectType } from 'tsd';
type Foo<T> = { foo: true, value: T };
type Bar = { foo: false };
type FooBar<T> = Foo<T> | Bar;
const fooInstance: Foo<number> = { foo: true, value: 42 };
const barInstance: Bar = { foo: false };
expectType<Foo<number>>( fooInstance ); // pass
expectNotType<Foo<number>>( barInstance ); // pass
expectNotType<Bar>( fooInstance ); // pass
expectType<Bar>( barInstance ); // pass
expectAssignable<FooBar<number>>( fooInstance ); // pass
expectAssignable<FooBar<number>>( barInstance ); // pass
type GetFoo<T> = () => Foo<T>;
type GetFooBar<T> = () => FooBar<T>;
const getFooInstance = () => fooInstance;
const getBarInstance = () => barInstance;
const getFooBarInstance = () => fooInstance as FooBar<number>;
expectType<GetFoo<number>>( getFooInstance ); // pass
expectNotType<GetFoo<number>>( getBarInstance ); // pass
expectNotType<GetFoo<number>>( getFooBarInstance ); // pass
expectAssignable<GetFooBar<number>>( getFooInstance ); // pass
expectAssignable<GetFooBar<number>>( getBarInstance ); // pass
expectType<GetFooBar<number>>( getFooBarInstance ); // pass
export type GetterFromFooBar<R,T1,T2 extends GetFooBar<T1>> =
[T2] extends [GetFoo<T1>]
? GetFoo<R>
: GetFooBar<R>;
// How to fix this function to properly narrow down return type to GetFoo
// when given a getter of type GetFoo,
// without requiring explicit type parameters at call site?
function processFooBar<R,T1,T2 extends GetFooBar<T1>>(
getter: T2,
mapper: (input: T1) => R
) {
return (() => {
const fooBar = getter();
return (fooBar.foo ? ({ foo: true, value: mapper(fooBar.value) }) : ({ foo: false }) );
}) as GetterFromFooBar<R,T1,T2>;
}
expectType<GetFoo<number>>( processFooBar<number, number, GetFoo<number>>(getFooInstance, x => x) ); // pass, explicit types
expectNotType<GetFoo<number>>( processFooBar<number, number, GetFooBar<number>>(getBarInstance, x => x) ); // pass, explicit types
expectNotType<GetFoo<number>>( processFooBar<number, number, GetFooBar<number>>(getFooBarInstance, x => x) ); // pass, explicit types
expectAssignable<GetFooBar<number>>( processFooBar<number, number, GetFoo<number>>(getFooInstance, x => x) ); // pass, explicit types
expectAssignable<GetFooBar<number>>( processFooBar<number, number, GetFooBar<number>>(getBarInstance, x => x) ); // pass, explicit types
expectType<GetFooBar<number>>( processFooBar<number, number, GetFooBar<number>>(getFooBarInstance, x => x) ); // pass, explicit types
expectType<GetFoo<number>>( processFooBar(getFooInstance, x => x) ); // Argument of type 'GetFoo<unknown>' is not assignable to parameter of type 'GetFoo<number>'.
expectNotType<GetFoo<number>>( processFooBar(getBarInstance, x => x) ); // pass, but GetFooBar<unknown> is nat expected
expectNotType<GetFoo<number>>( processFooBar(getFooBarInstance, x => x) ); // pass, but GetFooBar<unknown> is not expected
expectAssignable<GetFooBar<number>>( processFooBar(getFooInstance, x => x) ); // Argument of type 'GetFoo<unknown>' is not assignable to parameter of type 'GetFooBar<number>'.
expectAssignable<GetFooBar<number>>( processFooBar(getBarInstance, x => x) ); // Argument of type 'GetFooBar<unknown>' is not assignable to parameter of type 'GetFooBar<number>'.
expectType<GetFooBar<number>>( processFooBar(getFooBarInstance, x => x) ); // Argument of type 'GetFooBar<unknown>' is not assignable to parameter of type 'GetFooBar<number>'.
GetFoo
rather than () => Foo
) and without requiring to specify type parameters when calling the function? (Preferably without external dependencies.)I know it should be possible to just define function overloads in this simple example. That's actually how my actual code currently works. But it shows its limits elsewhere and I thought I'd try to introduce some smarter type inference.
Below is the simpler code I provided initially.
import { expectAssignable, expectNotType, expectType } from 'tsd';
type Foo = { foo: true, value: string };
type Bar = { foo: false };
type FooBar = Foo | Bar;
const fooInstance: Foo = { foo: true, value: '' };
const barInstance: Bar = { foo: false };
expectType<Foo>( fooInstance ); // pass
expectNotType<Foo>( barInstance ); // pass
expectNotType<Bar>( fooInstance ); // pass
expectType<Bar>( barInstance ); // pass
expectAssignable<FooBar>( fooInstance ); // pass
expectAssignable<FooBar>( barInstance ); // pass
type GetFoo = () => Foo;
type GetBar = () => Bar;
type GetFooBar = () => FooBar;
const getFooInstance = () => fooInstance;
const getBarInstance = () => barInstance;
const getFooBarInstance = () => fooInstance as Foo | Bar;
expectType<GetFoo>( getFooInstance ); // pass
expectNotType<GetFoo>( getBarInstance ); // pass
expectNotType<GetFoo>( getFooBarInstance ); // pass
expectNotType<GetBar>( getFooInstance ); // pass
expectType<GetBar>( getBarInstance ); // pass
expectNotType<GetBar>( getFooBarInstance ); // pass
expectAssignable<GetFooBar>( getFooInstance ); // pass
expectAssignable<GetFooBar>( getBarInstance ); // pass
expectType<GetFooBar>( getFooBarInstance ); // pass
export type GetterFromFooBar<T extends FooBar> = T extends Foo ? GetFoo : GetFooBar;
// How to fix this function to properly narrow down return type to GetFoo when given a getter of type GetFoo?
// Inlining GetterFromFooBar just changes the inferred type to GetFooBar.
function processFooBar(getter: GetFooBar) : GetterFromFooBar<ReturnType<typeof getter>> {
return getter;
}
expectType<GetFoo>( processFooBar(getFooInstance) ); // Argument of type 'GetFoo | GetFooBar' is not assignable to parameter of type 'GetFoo'.
expectNotType<GetFoo>( processFooBar(getBarInstance) ); // pass, but for wrong reason
expectNotType<GetFoo>( processFooBar(getFooBarInstance) ); // pass, but for wrong reason
expectNotType<GetBar>( processFooBar(getFooInstance) ); // pass, but for wrong reason
expectType<GetBar>( processFooBar(getBarInstance) ); // Argument of type 'GetFoo | GetFooBar' is not assignable to parameter of type 'GetBar'.
expectNotType<GetBar>( processFooBar(getFooBarInstance) ); // pass, but for wrong reason
expectAssignable<GetFooBar>( processFooBar(getFooInstance) ); // pass, but for wrong reason
expectAssignable<GetFooBar>( processFooBar(getBarInstance) ); // pass, but for wrong reason
expectType<GetFooBar>( processFooBar(getFooBarInstance) ); // Parameter type GetFooBar is not identical to argument type GetFoo | GetFooBar.
@ghybs and @jcalz offered a solution that doesn't preserve the named return type and doesn't seem to be possible to modify to have return type to be derived rather than equal to input type:
function processFooBar<T extends FooBar>(getter: () => T): () => T {
return getter;
}
@jcalz offered a working solution like this:
export type GetterFromFooBar<T extends FooBar> =
[T] extends [Foo] ? GetFoo :
[T] extends [Bar] ? GetBar :
GetFooBar;
function processFooBar<T extends FooBar>(getter: () => T) {
return getter as GetterFromFooBar<T>;
}
Unfortunately, I oversimplified the initial example, and this wasn't the entirety of my issue.
Solved it.
No need for utility types - it is possible to infer everything into optional type parameters.
function processFooBar<
R2,
TGetter1 extends GetFooBar<any>,
R1 = TGetter1 extends GetFooBar<infer T> ? T : never,
TGetter2 = TGetter1 extends GetFoo<R1> ? GetFoo<R2> : GetFooBar<R2>
>(
getter: TGetter1,
mapper: (input: R1) => R2
) {
return (() => {
const fooBar = getter();
return fooBar.foo ?
{ foo: true, value: mapper(fooBar.value as R1) }
: { foo: false };
}) as TGetter2;
}
const getFooInstanceWrapped = processFooBar(getFooInstance, x => x);
const getBarInstanceWrapped = processFooBar(getBarInstance, x => x);
const getFooBarInstanceWrapped = processFooBar(getFooBarInstance, x => x);
expectType<GetFoo<number>>( getFooInstanceWrapped ); // pass
expectNotType<GetFoo<number>>( getBarInstanceWrapped ); // pass
expectNotType<GetFoo<number>>( getFooBarInstanceWrapped ); // pass
expectAssignable<GetFooBar<number>>( getFooInstanceWrapped ); // pass
expectType<GetFooBar<unknown>>( getBarInstanceWrapped ); // pass (Bar has no generic type attached to it so it is expected this way without extra hints)
expectType<GetFooBar<number>>( getFooBarInstanceWrapped ); // pass
Ordering of type parameters is not pretty. It is possible to bring TMapper
into type parameters and derive R2
from that for a more natural ordering.
Definition of processFooBar
starts to look quite scary, but type resolution for inputs and outputs works as expected.
Update: While this solves the question the way I formulated it, there is still no full parity with function overloading - this may collapse generic parameters earlier than needed when they are not inferable from arguments, whilst function overloading keeps generic parameters.