I'm having an issue the typescript generics:
function isString(a: any): a is string {
return typeof a === 'string'
}
function concat<T extends string | number>(a: T, b: T): T {
if (isString(a) && isString(b)) {
return a.concat(b)
}
return a + b
}
Playground url: https://www.typescriptlang.org/play/index.html#src=function%20isString(a%3A%20any)%3A%20a%20is%20string%20%7B%0D%0A%20%20%20%20return%20typeof%20a%20%3D%3D%3D%20 'string'%0D%0A%7D%0D%0A%0D%0Afunction%20concat%3CT%20extends%20string%20%7C%20number%3E(a%3A%20T%2C%20b%3A%20T)%3A%20T%20%7B%0D%0A%20%20%20%20if%20(isString(a)%20%26%26%20isString(b))%20%7B%0D%0A%20%20%20%20%20%20%20%20return%20a.concat(b)%0D%0A%20%20%20%20%7D%0D%0A%20%20%20%20return%20a%20%2B%20b%0D%0A%7D%0D%0A
The typing seems appropriate but I have some errors. There seems to be some confusions around typescript generics but none of the answers I found helped me with that basic use case.
TypeScript does not narrow generic types via control flow (see microsoft/TypeScript#24085 ). So even if the type of a
is known to be string
, the type of T
will remain stubbornly T
. The only way to make your code compile as-is is to use type assertions to placate the compiler (as mentioned in the comments):
function concat<T extends string | number>(a: T, b: T): T {
if (isString(a) && isString(b)) {
return a.concat(b) as T; // assert as T
}
return (a as number) + (b as number) as T; // assert as numbers and T
}
Warning: when you use type assertions you need to be very careful not to lie to the compiler. Which we have, as you can see from the following situations:
// string literal types
const oops1 = concat("a", "b");
// type "a" | "b" at compile time, but "ab" at runtime
// numeric literal types
const oops2 = concat(5, 6);
// type 5 | 6 at compile time, but 11 at runtime
// string | number types
let notSure = Math.random() < 0.5 ? "a" : 1
const oops3 = concat(notSure, 100); // no error
// I bet you didn't want concat() to possibly accept string + number
The biggest problem is that T extends string | number
T extends string | number
will prompt the compiler to infer T
as a string literal type or a numeric literal type if it can. When you pass a string literal like "a"
in as a parameter, T
will be narrowed down to "a"
, meaning T
is only the string "a"
and no other value. I assume you don't want that.
The kind of function you're making is something that you would traditionally (before TS2.8 anyway) use overloads to accomplish:
function concat(a: string, b: string): string;
function concat(a: number, b: number): number;
function concat(a: string | number, b: string | number): string | number {
if (isString(a) && isString(b)) {
return a.concat(b);
}
return (a as number) + (b as number);
}
Now those examples will behave as you expect:
const oops1 = concat("a", "b"); // string
const oops2 = concat(5, 6); // number
let notSure = Math.random() < 0.5 ? "a" : 1
const oops3 = concat(notSure, 100); // error, notSure not allowed
You can get the same behavior using generics and conditional types , but it's probably not worth it:
type StringOrNumber<T extends string | number> =
[T] extends [string] ? string :
[T] extends [number] ? number : never
function concat<T extends string | number>(
a: T,
b: StringOrNumber<T>
): StringOrNumber<T> {
if (isString(a) && isString(b)) {
return a.concat(b) as any;
}
return (a as number) + (b as number) as any;
}
const oops1 = concat("a", "b"); // string
const oops2 = concat(5, 6); // number
let notSure = Math.random() < 0.5 ? "a" : 1
const oops3 = concat(notSure, 100); // error
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.