Вывод аргумента универсального типа класса завершается ошибкой в ​​зависимости от свойств этого класса

У меня довольно простой случай, когда Typescript не выводит типы. Я хотел бы знать, почему 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 не встречается ни в одной из подписей членов класса?

Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
1
0
140
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

См. запись часто задаваемых вопросов по 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

Площадка ссылка на код

Другие вопросы по теме