У меня довольно простой случай, когда Typescript не выводит типы. Я хотел бы знать, почему Typescript ведет себя таким образом.
Если у вас есть следующие объявления классов:
declare class Swine
{
grunt(): void
}
declare abstract class Part<T> {}
declare class Hoof extends Part<Swine> {}
И следующая функция:
declare function getOwner<T>(part: Part<T>): T;
Затем следующий код не работает в компиляторе Typescript:
getOwner(new Hoof())
.grunt();
Это связано с тем, что Typescript определяет возвращаемый тип getOwner как unknown
, а не Swine
.
Если я просто добавлю свойство animal: T
к объявлению Part
, код начнет работать.
Другими словами, если объявление Part
становится:
declare abstract class Part<T>
{
animal: T
}
Typescript сможет вывести тип возвращаемого значения getOwner(new Hoof())
. Я проверил это поведение для Typescript 3.9.7 и 4.1.3.
Почему Typescript ведет себя так? Должен ли он не хранить информацию об универсальном типе, даже если T
не встречается ни в одной из подписей членов класса?
См. запись часто задаваемых вопросов по TypeScript о неиспользуемых параметрах типа для получения канонического ответа.
Система типов TypeScript в основном структурная, а не номинальная. Это означает, что если вы сравниваете типы A
и B
, они считаются одним и тем же типом тогда и только тогда, когда они имеют одинаковую структуру (например, оба являются объектными типами с элементами одних и тех же ключей и одинаковыми типами значений). Неважно, какие типы A
и B
названы (например, тот факт, что я использую такие имена, как «A
» и «B
», не имеет значения) или где они объявлены (например, наличие отдельных объявлений, таких как interface A { a: string }
и interface B { a: string }
, не имеет значения). т сделать их отдельными типами).
В частности, с учетом декларации
declare abstract class Part<T> { }
Вы можете видеть, что независимо от того, какой тип вы укажете для T
, результирующий тип является пустым типом объекта без членов и, следовательно, имеет тот же тип, что и просто {}
. Компилятор действительно не видит никакой разницы между Part<swine>
и Part<string>
или чем-то еще.
Чтобы увидеть это, обратите внимание, что TypeScript не позволит вам повторно объявить var
с аннотацией типа, отличного от исходного объявления:
var bad: Swine;
var bad: string; // error!
// ~~~
// Subsequent variable declarations must have the same type.
Но ни одно из следующих действий не приводит к ошибке компилятора:
var okay: Part<Swine>;
var okay: {}; // no error
var okay: Part<string>; // no error
var okay: Part<unknown>; // no error
Так что они действительно одного типа в TypeScript. И это означает, что когда вы передаете компилятору значение типа Part<Swine>
, в нем нет ничего похожего на Swine
. Имя Part<Swine>
— это просто имя, и оно не должно влиять на то, как компилятор обрабатывает тип. Таким образом, попытка вывести T
из значения типа Part<T>
в принципе невозможна; вывод терпит неудачу, и вы получаете unknown
.
Добавление свойства типа T
к Part<T>
меняет все. Теперь есть
структурная разница между Part<Swine>
и Part<string>
или Part<unknown>
или {}
, и у компилятора есть дескриптор, по которому можно определить T
из Part<T>
.
Если представить, что мы перенесем это с уровня типов на уровень значений, взглянув на функции, которые принимают строковые представления типов и превращают их в другие строковые представления типов, аналог того, что вы делали, будет примерно таким:
function part(T: string): string {
return "{}";
}
const hoof = part("Swine");
function getOwnerType(partT: string): string {
// how would you implement this ???
return "unknown";
}
console.info(getOwnerType(hoof)); // unknown
Перевод declare abstract class Part<T> {}
просто (T: string) => "{}"
). Поскольку part("Swine")
выдает тот же результат, что и part("string"}
или что-то еще, а именно строку "{}"
, нет возможности написать обратную функцию getOwnerType()
. Если, с другой стороны, ваша функция part()
имеет вывод, который на самом деле зависит от ее ввода, все становится более разумным:
function part(T: string): string {
return "{animal: " + T + "}";
}
const hoof = part("Swine");
function getOwnerType(partT: string): string {
return (partT.match(/^{animal: (.*)}$/) ?? ["", "unknown"])[1];
}
console.info(getOwnerType(hoof)); // Swine