TypeScript полиморфен этому типу в интерфейсах

Можно ли эмулировать поведение полиморфного типа this TypeScript, используя только интерфейсы вместо классов? Предположим, у меня есть такой код:

interface Foo {
  foo(): this;
}

interface Bar extends Foo {
  bar(): this;
}

function foo(this: Foo) { return this; }
function bar(this: Bar) { return this; }

function newFoo(): Foo {
  return { foo };
}

function newBar(): Bar {
  return { ...newFoo(), bar }; // Property 'bar' is missing in type 'Foo' but required in type 'Bar'.
}

Конструктор newBar() не выполняет проверку типа, поскольку возвращаемый тип foo(), полученный из деструктурированного newFoo(), выводится как Foo вместо Bar.

Я обнаружил, что работает обратный метод упаковки миксинов:

function fooish<T>(object: T): T & Foo {
  return { ...object, foo };
}

function newBar2(): Bar {
  return fooish({ bar }); // Typechecks fine
}

Однако в некоторых ситуациях я бы предпочел первый метод, т. е. иметь простой конструктор, который просто возвращает Foo вместо T & Foo и компонует путем распространения свойств, а не приложения. По сути, я ищу наследование с интерфейсами, чтобы this оставался набранным правильно.

Есть идеи?

Я этого не понимаю. newFoo возвращает Foo, а не this. Внутри Bar вам нужен метод foo(), чтобы вернуть Bar, а не Foo, поскольку this теперь Bar. Я не понимаю, чего вы пытаетесь достичь и как то, что вы делаете, должно быть типобезопасным. Где конкретно вы пытаетесь «подражать» типу this? Я имею в виду, что вы используете this в своем коде, так что, по-видимому, на самом деле он должен использовать тип, а не эмулировать его. Я здесь безнадежно запутался. Не могли бы вы отредактировать, чтобы уточнить?

jcalz 13.04.2024 14:48

Извините, если я не ясно выразился. Если у меня есть класс Foo с методом foo, который возвращает экземпляр Foo обратно, т. е. return this, и если я создаю его подкласс как Bar extends Foo, то метод foo на Bar будет иметь возвращаемый тип Bar (вместо Foo). Я хотел бы сделать что-то подобное с интерфейсами. Не знаю, как это сформулировать в редакции.

Adam B. 13.04.2024 15:47

• Вероятно, вы говорите, что хотите, чтобы foo() и bar() вернулись this тогда. Не интерфейсам нужно что-то особенное (поскольку интерфейсы прекрасно поддерживают this), а автономные функции, которые вы используете в качестве методов. Я был бы признателен, если бы вы могли отредактировать , чтобы сказать это, но если вы не можете понять, как это сделать, я могу через некоторое время внести свои собственные изменения. • Возможно этот подход может работать, когда вы используете дженерики для обработки необходимого полиморфизма (this реализован как неявный дженерик-тип). Если да, то я напишу ответ; если нет, то чего не хватает?

jcalz 13.04.2024 15:50

Вероятно, я что-то упускаю, но не понимаю, в чем проблема с функциями, например. function newBar() { return { ...({} as Foo), bar }; }; показывает ту же ошибку.

Adam B. 13.04.2024 15:54

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

Adam B. 13.04.2024 15:59

Чтобы {...({} as Foo), bar} было Bar, это as Foo должно быть as Pick<Bar, "foo">. Часть foo в Foo не обязательно должна соответствовать Bar. Может быть, вы не понимаете, как работает полиморфный this и что он означает? Единственный возможный способ обеспечить типобезопасность вашего кода — это если известно, что метод foo() возвращает this для текущего this. Дело не в том, что Foo.foo возвращается Foo и Bar.bar возвращается Bar.

jcalz 13.04.2024 16:01

Хорошо, понятно, спасибо, что уделили время @jcalz, как всегда великолепно. Ваш ответ работает хорошо, немного раздражает, что мне приходится добавлять префикс к каждому методу, который возвращает this, с общим префиксом, но я думаю, это имеет смысл. Напишите ответ и я одобрю его.

Adam B. 13.04.2024 16:04

Я сделаю это, когда у меня будет возможность.

jcalz 13.04.2024 16:15

Вероятно, это произойдет не раньше, чем через несколько часов.

jcalz 13.04.2024 16:36
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой 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
9
79
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Полиморфный тип неявно является универсальным . Тип this — это особый тип F-ограниченного полиморфизма , называемый « любопытно повторяющимся шаблоном шаблона в C++. Для реализатора типа this — это «некоторый общий подтип текущего типа, который я пишу», тогда как для пользователь типа, this является этим типом. Если вы хотите реализовать Foo, вам необходимо убедиться, что метод foo() возвращает любой подтип Foo, который будет использоваться позже.

Ваш код по существу эквивалентен

interface Foo<T extends Foo<T>> {
  foo(): T;
}
interface PlainFoo extends Foo<PlainFoo> {}

interface Bar<T extends Bar<T>> extends Foo<T> {
  bar(): T;
}
interface PlainBar extends Bar<PlainBar> {}

function foo(this: PlainFoo) { return this; }
function bar(this: PlainBar) { return this; }

function newFoo(): PlainFoo {
  return { foo };
}

function newBar(): PlainBar {
  return { ...newFoo(), bar }; 
  // Property 'bar' is missing in type 'Foo<PlainFoo>' 
  // but required in type 'Bar<PlainBar>'
}

где теперь, надеюсь, понятно, в чем проблема. Ваш newBar() подразумевает возврат PlainBar, но это Bar<PlainBar>, то есть Foo<PlainBar>, и поэтому метод foo() должен возвращать PlainBar. Но вместо этого он возвращает PlainFoo. Тип this должен сужаться в подклассах, вот в чем вся суть.


Итак, чтобы ваш код работал с

interface Foo { foo(): this; }
interface Bar extends Foo { bar(): this; }

Вам нужны foo и bar, чтобы вернуть this, а не Foo и Bar. Это означает, что универсальный тип this должен быть явно представлен как параметр универсального типа:

function foo<T extends Foo>(this: T) { return this; }
function bar<T extends Bar>(this: T) { return this; }

И теперь все работает (и вы также не хотите аннотировать возвращаемый тип newFoo() как Foo, поскольку это забивает общий:

function newFoo() {
  return { foo };
}

function newBar(): Bar {
  return { ...newFoo(), bar };
}

Теперь все это работает, поскольку полиморфная природа this сохраняется.

Ссылка на код детской площадки

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