The error message you encounter in your examples stems from the ability to explicitly specify type arguments when invoking a function. For instance:
fooB<number>(42, (i) => i);
Considering fooB
:
const fooB = <R1>(value: number = 42, bar: <R0>(val: number) => R0) => {
return bar<R1>(value);
};
Internally, bar
can potentially be called with any other type besides R1
. This lack of constraint means that there is no guarantee that the type of value
(which is number
) aligns with the expected type R0
.
The issue surfaces when attempting this scenario:
fooB<21>(42, (i) => i);
In this case, R1
gets instantiated as 21
.
Here, 42 extends number
, which is expected based on the type of value
, but number extends 21
is FALSE. In simpler terms, 42
and 21
are distinct subtypes of the type number
. Consequently, the returned value i
cannot be assigned to either R1
or R0
. A similar dilemma arises with foo0B
and R0
.
There's a comprehensive discussion available on why this error occurs at Stack Overflow.
The Solution
If I understand correctly, you desire a function that:
- Takes an argument of type
T
- Optionally accepts a function
fn
that works with an input of type T
and outputs a value of type U
- Invokes
fn
with the provided argument, returning the result (U
) if fn
is given, or returning T
otherwise (the identity).
The direct approach to implement this functionality would be:
function foo<T, U>(value: T, fn: (val: T) => T | U = i => i): T | U {
return fn(value);
}
However, both foo
and fn
in this scenario can only return the union type T | U
due to having just one signature that covers all cases.
- No
fn
provided
fn
provided
If you wish for type resolution to vary depending on whether a callback is passed (assumed based on the default I => I
), you can opt for function overloading. Essentially, you are declaring two signatures for foo
:
function foo<T>(value: T): T;
function foo<T, U>>(value: T, fn: (val: T) => U): U;
function foo<T>(value: T, fn = (i: T) => i) {
return fn(value);
}
The first signature accounts for scenarios where no callback is supplied, taking a T
and returning a T
. In essence, foo
behaves like the identity function here.
The second signature expects the presence of a callback function fn
. If no explicit type arguments are specified during a call to foo
, then the type U
will be inferred from the type of fn
. Otherwise, the type of fn
must match the provided U
.
The subsequent function implementation caters to both signatures using the identity function as the default value to align with the first signature, or utilizing a custom function compatible with the second. The implementation signature specifically defines the types that remain constant across various overloads, hence solely mentioning
T</code. The overloads handle specifying the return type on a per-call basis.</p>
<p>Feel free to explore a working example on <a href="https://www.typescriptlang.org/play/#code/GYVwdgxgLglg9mABMOcA8AVAfACgG4CGANiAKYBciGAlJRgNwBQoksCyqmANIgKq6ESFKj2BhK+YnWqIAvFj60+TFtHhIU6bJKF1RSWYhwxpchTBkBvRoluIATqSgh7GsDrLUmAX0aMA9P6IAOowUAAWcCBQiBDERABGBBAA1owQCADOMQSUYCAAtgmk9nIccDgALABMXnb1DYiBiABiMGCk6VkxCZTZ9u0A5mWaOABEAJIA5AWIBIj9Q2N1zW0dXWDZsX1QA2DDhqM1dY2NzQCiAEqXAPKXlFAAngAOpIhTNVOIMJmIYHA5TKZGCDMAEBJEN5QOCIJ6vd6LfZTPzNUIRWLEJKpDZbAAmeUKxVKh1QOA+1SmPGeBHsmVIEzAUBOpxZTSCNzSGU2MWEiIO5Sq1R4OAAHgSiiUZPJECKAHTQgDKuyGOGoK3ZnO6yB2e35RyFRjFf0JkrMMsQAGpEABGZkNC7XO4PF5vKb5CX2L4-P4AuZAkFgiFQmFw1185EBIJo8KIUgi55EGAQMKwl2ZHExQbiokjTh8njuom4cmUxDU2n0xknZocjOIcLZkq59D540e4ufYUiqUKbv0WwO273VPwqbh76-f6A4Gg8GQ2Ehl3vQslKZAA" rel="nofollow noreferrer">TypeScript Playground</a>, featuring the following use-cases:</p>
<pre><code>// No callback
const x1: number = foo(42); // Success
const x2: string = foo("I'm a string"); // Success
const x3: string = foo(42); // ERROR: '42' is not assignable to 'string'
// With callback
const x4: number = foo('42', parseInt); // Ok
const x5: string = foo(42, (x: number) => x.toString()); // Ok
const x6: string = foo(42, (x: number) => x + 1); // ERROR: 'number' is not assignable to 'string'
// Explicit types
const x7: number = foo<string, number>('42', parseInt); // Ok
const x8: number = foo<string, number>('42', (x) => x); // ERROR: 'string' is not assignable to 'number'