Расширение интерфейса с универсальным, больше не назначаемым родительскому

Недавно я обновился до typescript 2.4 и получил несколько ошибок, жалующихся на то, что мои типы больше нельзя назначать.

Вот сценарий, в котором я сталкиваюсь с ошибкой:

interface Parent {
    prop: any
}

interface Child extends Parent {
    childProp: any
}

type Foo<T> = <P extends Parent>(parent: P) => T

function createFooFunction<T>(arg: T): Foo<T> {
    // Error here!
    return (child: Child): T => {
        return arg;
    }
}

В typescript 2.3 это допустимо, но typescript 2.4 выдает эту ошибку

Type '(child: Child) => T' is not assignable to type 'Foo<T>'.
Types of parameters 'child' and 'parent' are incompatible.
    Type 'P' is not assignable to type 'Child'.
    Type 'Parent' is not assignable to type 'Child'.
        Property 'childProp' is missing in type 'Parent'.

Что касается последней строки ошибки, я заметил, что если я сделаю свойства Child необязательными, то машинопись будет удовлетворена, т.е. если я сделаю это изменение

interface Child extends Parent {
    childProp?: any
}

Хотя это не идеальное решение, так как в моем случае требуется childProp.

Я также заметил, что изменение типа аргумента Foo непосредственно на Parent также удовлетворит машинописный текст, т.е. внесение этого изменения

type Foo<T> = (parent: Parent) => T

Это тоже не исправление, так как я не контролирую ни тип Foo, ни Parent. Оба они взяты из файлов поставщика .d, поэтому я не могу их изменить.

Но в любом случае, я не уверен, что понимаю, почему это ошибка. Тип Foo говорит, что ему требуется что-то, что расширяет Parent, и Child является таким объектом, так почему же typescript считает, что его нельзя назначать?

Изменить: я пометил это как ответ, поскольку добавление флага --noStrictGenericChecks подавляет ошибки (принятый ответ здесь). Тем не менее, я все же хотел бы знать, почему это ошибка, в первую очередь, поскольку я бы предпочел сохранить строгие проверки и реорганизовать свой код, если он неправильный, а не просто замкнуть его.

Итак, чтобы повторить суть вопроса, поскольку Child расширяет Parent, почему машинописный текст больше не думает, что Child можно присвоить Parent, и с точки зрения дженериков ООП, почему это более правильно, чем было раньше?


person davidmk    schedule 05.07.2017    source источник


Ответы (2)


Версия 2.4 вводит более строгую проверку дженериков.

Прочтите это https://blogs.msdn.microsoft.com/typescript/2017/06/27/announcing-typescript-2-4/ и найдите заголовок Более строгая проверка дженериков.

Из статьи:

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

Он также вводит строгую контравариантность для обратных вызовов.

person Peter Wone    schedule 05.07.2017
comment
Добавление --noStrictGenericChecks действительно подавило ошибки, поэтому я отмечу это как ответ. - person davidmk; 05.07.2017

Я думаю, проблема в том, что вы возвращаете функцию, которая требует Child в качестве аргумента, но назначаете ее функции, которая должна иметь возможность принимать все, что происходит от Parent, а не только Child.

Допустим, вы вызываете createFooFunction<T> для получения Foo<T>. По определению Foo<T> можно создать класс Child2, расширяющий Parent, и передать его в качестве аргумента полученному Foo<T>. Это проблема, поскольку вы на самом деле вернули функцию, которая может получить только Child в качестве аргумента, но не Child2.

Это на самом деле более правильно. Помните, что вы не берете Child и не назначаете его Parent. Вы берете функцию, которая ожидает только Child, и назначаете ее функции, которая может принимать любые Parent. Это совсем другое. С другой стороны, если бы мы имели дело с возвращаемым значением, а не с вводом, все было бы в порядке. В этом разница между ковариантностью и контравариантностью, которая поначалу может немного сбивать с толку. В C# вы сможете указать это в своих собственных универсальных классах, используя ключевые слова in и out.

Например, в C#, если у вас есть IEnumerable<Child> (объявленный как IEnumerable<out T>), это также, естественно, IEnumerable<Parent>, поскольку вы выполняете итерацию — вы получаете Child объектов — вы также можете получить Parent объектов, потому что каждый Child также является Parent. Следовательно, вы можете присвоить IEnumerable<Child> IEnumerable<Parent>, но не наоборот! Поскольку вам не гарантировано получение объекта Child.

С другой стороны, если у вас есть что-то вроде IComparer<Parent> (объявлено как IComparer<in T>), которое может сравнивать два объекта Parent, ну, поскольку каждый Child также является Parent, он также может сравнивать любые два объекта Child. Таким образом, вы можете присвоить IComparer<Parent> IComparer<Child>, но не наоборот - то, что может сравнивать Child, умеет сравнивать только Child! В чем твоя проблема здесь.

Вы можете понимать in и out как входные (аргументы) и выходные (возвращаемые) значения в обратных вызовах. Вы можете только сделать входные данные более конкретными (контравариантность), а выходные данные более общими (ковариантность).

Кстати, я думаю, что <P extends Parent> здесь совершенно бесполезна (и это добавляет путаницы), потому что вы все равно можете передать в эту функцию все, что расширяет Parent, даже если оно не является универсальным. Это было бы полезно только в том случае, если бы вы возвращали такой тип: <P extends Parent>(parent: P) => P, чтобы сохранить правильный тип P в возвращаемом значении. Без дженериков вам пришлось бы брать Parent и возвращать Parent, так что вы получите обратно Parent, даже если введете что-то более конкретное. Что касается того, почему ошибка исчезает, если вы избавитесь от этого, я действительно понятия не имею.

person Neme    schedule 16.07.2017