I would like to understand how to get where I want with generic type so this is my example :
function foo(value: 42 = 42 ,bar:(val: any) => any = I => I){
return bar(value)
}
I work with a value
of some type T
in my example the type is a number
so I am using 42
I want to have a function as an argument that will change the value to a different type R
or return a same type T
which in this case would be the identity 'I => I' as the default parameter value.
I rewrote my function as an arrow function like this:
const foo = <R1>(
value: number = 42,
bar: <R0>(val: number) => R0 = I => I
) => {
return bar<R1>(value);
};
const foo0 = (
value: number = 42,
bar: <R0>(val: number) => R0 = I => I
) => {
return bar(value);
};
const foo1 = <R1>(
value: number = 42,
bar: (val: number) => R1 = I => I
) => {
return bar(value);
};
ERROR: Type 'number' is not assignable to type 'RX'. 'RX' could be instantiated with an arbitrary type which could be unrelated to 'number'.
I am using R1
and R0
because I am not sure the difference between R one and R naught in this example but if I remove the I => I
default value the error message go away but I don't understand why or how to overcome it...
in the below example fooB(42,i=>i)
and foo0B(42,i=>i)
erored out but not foo1B(42,i=>i)
How can I set the default value I => I
without having to specify both R | number
const fooB = <R1>(value: number = 42, bar: <R0>(val: number) => R0) => {
return bar<R1>(value);
};
const foo0B = (value: number = 42, bar: <R0>(val: number) => R0) => {
return bar(value);
};
const foo1B = <R1>(value: number = 42, bar: (val: number) => R1) => {
return bar(value);
};
fooB(42,i=>i)
foo0B(42,i=>i)
foo1B(42,i=>i)
The error message that you get from your examples is caused by the fact that type arguments can be explicitly given when calling a function, for example:
fooB<number>(42, (i) => i);
considering fooB
:
const fooB = <R1>(value: number = 42, bar: <R0>(val: number) => R0) => {
return bar<R1>(value);
};
what happens here is that internally bar
can be called with any other type besides R1
too. If that is the case there is no guarantee that number
(the type of value
) is a type that's compatible with R0
.
The problem arises when we'd do for example:
fooB<21>(42, (i) => i);
About this call we can say: R1
is instantiated with/as 21
.
Here 42 extends number
as it's supposed to considering the type of value
, but number extends 21
is FALSE. In other words: 42
and 21
are different subtypes of type number
. Therefore the returned i
is not assingable to R1
nor R0
. A similar problem occurs with foo0B
and R0
.
There's already some good coverage on why this error message occurs.
If I understand correctly, you want a function that:
T
fn
that takes an argument of type T
and returns a value of type U
fn
with the first argument, and then returns the result (U
) if fn
is given, or returns T
otherwise (the identity).The most straightforward way to write this is like the following:
function foo<T, U>(value: T, fn: (val: T) => T | U = i => i): T | U {
return fn(value);
}
But here both foo
and fn
can indeed only return the union type T | U
, since there's only one signature to account for both the cases
fn
fn
If you want branching of types depending on whether a callback is passed (I'm assuming this based on the default I => I
) you can use function overloading instead. Essentially it's like saying "I have the following 2 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 function signature is for when no callback is passed: it takes a T
, and will just return a T
. In that sense foo
is like the identity function.
The second signature takes the callback fn
. If no type arguments are explicitly given to a call to foo
, U
is going to be inferred from the type of fn
. Otherwise the type of fn
will be validated against the given type for U
.
Then you have the function implementation. It can account for both signatures, and does so using the identity function (default value) to match the first signature, or a given function to match the second. This implementation signature only explicitly specifies the types that are constant for all overloads, so only T
is specified. The overloads will do the rest by determining the return type on a per-call basis.
Here's a complete TS playground using the following examples:
// Without callback
const a: number = foo(42); // Fine
const b: string = foo("I'm a string"); // Fine
const c: string = foo(42); // ERROR: type '42' is not assignable to type 'string'
// With callback
const d: number = foo('42', parseInt); // Ok
const e: string = foo(42, (x: number) => x.toString()); // Ok
const f: string = foo(42, (x: number) => x + 1); // ERROR: type 'number' is not assignable to type 'string'
// With explicit types
const g: number = foo<string, number>('42', parseInt); // Ok
const h: number = foo<string, number>('42', (x) => x); // ERROR: type 'string' is not assignable to type 'number'
Collected from the Internet
Please contact [email protected] to delete if infringement.
Comments