Typescript generic несовместим с типом возвращаемого значения

#typescript #generics #typescript-typings #typescript-generics

#typescript #дженерики #typescript-типизации #typescript-generics

Вопрос:

У меня возникла проблема с 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) amp;amp; isString(b)) {
        return a.concat(b)
    }
    return a   b
}
 

URL-адрес игровой площадки: https://www.typescriptlang.org/play/index.html#src=function isString(a: any): a is string {
return typeof a ===
‘string’
}

function concat(a: T, b: T): T {
if (isString(a) && isString(b)) {
return a.concat(b)
}
return a + b
}

Ввод текста кажется подходящим, но у меня есть некоторые ошибки. Кажется, есть некоторые недоразумения с typescript generics, но ни один из ответов, которые я нашел, не помог мне с этим базовым вариантом использования.

Комментарии:

1. Вам не хватает приведения : return a.concat(b) as T . А также: return Number(a) Number(b) as T

Ответ №1:

TypeScript не сужает общие типы с помощью потока управления (см. microsoft / TypeScript #24085). Таким образом, даже если известно, что тип a известен string , тип T будет упорно сохраняться T . Единственный способ заставить ваш код компилироваться как есть — это использовать утверждения типа для успокоения компилятора (как указано в комментариях):

 function concat<T extends string | number>(a: T, b: T): T {
    if (isString(a) amp;amp; 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
}
 

Предупреждение: при использовании утверждений типа вам нужно быть очень осторожным, чтобы не лгать компилятору. Что мы имеем, как вы можете видеть из следующих ситуаций:

 // 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
 

Самая большая проблема заключается в том, что T extends string | number компилятор будет запрашивать вывод T в виде строкового литерального типа или числового литерального типа, если это возможно. Когда вы передаете строковый литерал, подобный "a" in, в качестве параметра, T значение будет сужено до "a" , что означает T только строку "a" и никакого другого значения. Я предполагаю, что вы этого не хотите.

Создаваемая вами функция — это то, для выполнения чего вы традиционно (во всяком случае, до TS2.8) использовали перегрузки:

 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) amp;amp; isString(b)) {
        return a.concat(b);
    }
    return (a as number)   (b as number);
}
 

Теперь эти примеры будут вести себя так, как вы ожидаете:

 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
 

Вы можете получить такое же поведение, используя общие и условные типы, но, вероятно, оно того не стоит:

 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) amp;amp; 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