Regrettably, the TypeScript compiler struggles to comprehend the logic in your approach here. When you use
typeof value === "string"
, the compiler views it as a
type guard that narrows down
value
from
T
to either
string
or
number
. However, it does not narrow the type parameter
T
itself.
You were hoping for the compiler to interpret
typeof value === "string"
and
value: T
together and modify
T
from
T extends string | number
to
T extends string
or
T extends number
, or even just specific types like
string
and
number
. Unfortunately, this does not occur.
T
remains as
T
throughout the function, making
callStringFunction(genericCallback)
unacceptable. The compiler still perceives that
T
might be any arbitrary subtype of
string | number
.
There are ongoing feature requests on GitHub to address this situation in a better way; refer to microsoft/TypeScript#33014 for more details. However, no solution has been implemented yet, partly because certain "obvious" implementations could lead to unsoundness.
For instance, if I had
function f<T extends string | number>(value1: T, value2: T): void;
, I would not be able to assert that if
value1
is a
string
, then
value2
must also be a
string
. It is still possible for both
value1
and
value2
to be
string | number
, as shown in the function call
f(Math.random()<0.99 ? "" : 123, 123);
. There is a 99% chance of
value1
being a
string
, while
value2
is a
number</code. Any solution to address this would need careful consideration.</p>
<hr />
<p>Furthermore, I cannot find an effective way to restructure your code so that the compiler can guarantee its safety. The types <code>string
and
number
are not recognized as
discriminant property types, preventing me from transforming the argument list to
genericFunction()
into a
discriminated union. This approach only works if you pass in a literal parameter like
function genericFunction<K extends "str" | "num">(type: K, val: Val[K], cb: (x: Val[K])=>void): void;
. In that case, you could verify
type
, and then the compiler would narrow down both
val
and
cb
. The current structure of your code does not align with this, making it challenging to refactor directly.
As a temporary solution, I recommend using a type assertion to inform the compiler that you are aware of the type correctness. You can utilize val as Type
where you are confident that val
is indeed of type
Type</code, as demonstrated below:</p>
<pre><code>function genericFunction<T extends string | number>(
value: T, genericCallback: (newValue: T) => void
) {
if (typeof value === "string") {
console.log(value); // T is now identified as a string at this point
callStringFunction(genericCallback as (newValue: string) => void);
}
else {
console.log(value); // T is now identified as a number at this point
callNumberFunction(genericCallback as (newValue: number) => void);
}
}
This approach resolves the errors by shifting the responsibility of ensuring type safety onto you. While not ideal, it seems to be the best workaround for the time being, without transitioning to a visibly distinct algorithm. It's a compromise for now!
Click here to view code in Playground