Функция TypeScript передает проверку типа, когда этого не ожидается

#typescript #types #typescript-typings #typing

#typescript #типы #typescript-типизации #ввод

Вопрос:

У меня есть функция identityOrCall , которая либо вызывает заданную ей функцию, либо возвращает значение. value является универсальным типом.

 function identityOrCall <T>(value: T): T {
  if (typeof value === 'function')
    return value()
  else
    return value
}

identityOrCall(() => 'x') // -> 'x'
  

Строка identityOrCall(() => 'x') , похоже, проходит проверку типа компилятора.

Почему? Разве это не должно выдавать ошибку?

Если identityOrCall передается функция, я бы ожидал, что общий тип T будет установлен на Function , и это identityOrCall должно возвращать Function тип.

Вместо этого я могу передать Function тип в качестве аргумента и identityOrCall вернуть string тип.

Почему это так? Мне это кажется непоследовательным, но я, должно быть, что-то упускаю.

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

1. identityOrCall(() => 'x') не вернет ошибку (с чего бы это?) в этом случае T будет () => string , который является допустимым типом

2. @apokryfos Мое недоразумение заключается в том, что я ожидаю identityOrCall вернуть тот же тип, что и его аргумент. Если Function в качестве аргумента указан тип, я бы ожидал, что он вернет Function тип.

Ответ №1:

Проблема в том, что, поскольку тип возвращаемого значения функции нигде не указан, TypeScript принимает его тип равным any . any не является типобезопасным; его можно назначить чему угодно. Здесь TS извлекается T из any возвращаемого значения (поскольку any его можно сузить до T).

Лучше избегать типов any , когда это возможно. Мне нравится использовать TSLint no-unsafe-any , который запрещает подобные вещи.

По аналогичным причинам следующее не выдает ошибку TypeScript (и запрещено с использованием приведенного выше правила компоновки), хотя это явно небезопасно:

 declare const fn: () => any;
function identityOrCall<T>(value: T): T {
    return fn();
}

const result = identityOrCall(() => 'x') // -> 'x'
// result is typed as: () => 'x'. Huh?
  

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

1. Я не уверен, что это правильное объяснение. Я использую "noImplicitAny" в своем tsconfig.json . Это также, по-видимому, не приводит к ошибкам компиляции: identityOrCall(function (): string { return 'x' })

2. На самом деле, я думаю, что теперь понимаю. Таким T образом, здесь предполагается «самый слабый» тип, который есть any , в отличие от немедленного принятия типа первого T введенного аргумента.

Ответ №2:

Если цель состоит в том, чтобы метод вел себя по-разному в зависимости от параметров, я думаю, что самый простой способ — указать перегрузки

 function identityOrCall<T>(value: () => T): T;
function identityOrCall<T>(value: T): T;

function identityOrCall <T>(value: () => T|T): T {
  if (typeof value === 'function')
    return value()
  else
    return value
}

const result = identityOrCall(() => 'x') // first overload
const result2 = identityOrCall('x') // second overload

result.split(' ');  // No error
result2.split(' '); // No error
result(); // error not callable
  

При вызове метода TS разрешает сигнатуру, просматривая перегрузки, пока не найдет ту, которая соответствует (здесь имеет значение порядок определения перегрузок).

Ссылка на игровую площадку