Универсальный Typescript несовместим с типом возвращаемого значения - PullRequest
1 голос
/ 16 апреля 2019

У меня проблема с типами машинописного текста:

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
}

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

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

1 Ответ

2 голосов
/ 16 апреля 2019

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

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
}

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

// 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" в качестве параметра, 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) && 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) && 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

В любом случае, надеюсь, это поможет. Удачи!

...