The compiler rightly warns that returning "foo"
or 1
may not satisfy the generic return type T
. String and number literal types are available, so if you invoke dummy(123)
, then T
will be considered as 123
, and assigning 1
to 123
can cause runtime errors:
function dummy<T extends string | number>(input: T): T {
let result: T
if (typeof input === 'string') { result = 'foo' } else { result = 1 }
return result
}
const oops = dummy("abc");
// ^? const oops: "abc"
const obj = { abc: 123, def: 456 };
const num = obj[oops];
// ^? const num: number
num.toFixed() // no compiler error, but
// 💥 RUNTIME ERROR! num is undefined 💥
In this scenario, the compiler assumes oops
is of type "abc"
, enabling indexing into an object with a key of "abc"
, which leads to issues.
You have various ways to correct your typings, though the compiler cannot ensure your implementation satisfies those typings accurately.
One approach is changing the function's return type from T
to a conditional type. This way, it outputs string
for T extends string
and number
otherwise, ensuring the right type even when T
is narrower than string
or number
:
declare function dummy<T extends string | number>(
input: T
): T extends string ? string : number;
const str = dummy("abc");
// const str: string
const num = dummy(123);
// const num: number
This modification helps from the caller's perspective. However, the compiler still might report issues with the implementation due to its logic complexities. Check microsoft/TypeScript#33912 for related feature requests. For now, consider using type assertions to silence warnings:
function dummy<T extends string | number>(input: T): T extends string ? string : number {
let result: T extends string ? string : number;
if (typeof input === 'string') {
result = 'foo' as typeof result // <-- assert
} else {
result = 1 as typeof result // <-- assert
}
return result
}
Another option involves overloading your function with multiple call signatures:
declare function dummy(input: string): string;
declare function dummy(input: number): number;
const str = dummy("abc");
// const str: string
const num = dummy(123);
// const num: number
While this solution works during function calls, the implementation does not guarantee that proper output corresponds to each input value:
function dummy(input: string): string;
function dummy(input: number): number;
function dummy(input: string | number) {
let result: string | number;
if (typeof input === 'string') {
result = 'foo' // acceptable, but result = 1 would also be fine here
} else {
result = 1 // valid, however result = "foo" would also work here
}
return result
}
You may combine both strategies by utilizing a single generic overload call signature:
// call signature is generic
function dummy<T extends string | number>(
input: T
): T extends string ? string : number;
// implementation is not
function dummy(input: string | number) {
let result: string | number;
if (typeof input === 'string') {
result = 'foo';
} else {
result = 1;
}
return result
}
Although not entirely type-safe (changing typeof input === 'string'
to typeof input !== 'string
won't alert the compiler), this method might offer more convenience in certain cases.
Playground link showcasing code updates