strictFunctionTypes ограничивает тип c generi - PullRequest
2 голосов
/ 09 мая 2020

Проблема, кажется, связана с c тем, как strictFunctionTypes влияет на generi c тип класса.

Вот класс, который точно воспроизводит то, что происходит, и не может быть упрощен далее из-за требований any используется для обозначения частей, которые не накладывают дополнительных ограничений ( игровая площадка ):

class Foo<T> {
    static manyFoo(): Foo<any[] | { [s: string]: any }>;
    static manyFoo(): Foo<any[]> {
        return ['stub'] as any;
    }

    barCallback!: (val: T) => void;

    constructor() {
        // get synchronously from elsewhere
        (callback => {
            this.barCallback = callback;
        })((v: any) => {});
    }

    baz(callback: ((val: T) => void)): void {}
}

T generi c введите barCallback подпись вызывает ошибку типа:

(method) Foo<T>.manyFoo(): Foo<any[]>
This overload signature is not compatible with its implementation signature.(2394)

Проблема появляется, только если T используется как val тип в barCallback типе функции.

Он исчезает, если barCallback или baz не используйте T в качестве типа параметра:

barCallback!: (val: any) => void | T;

Он исчезает, если нет перегрузок метода manyFoo или подписи менее разнообразны.

Это не появляются, если barCallback имеет сигнатуру метода в классе, но это предотвращает его присвоение позже:

barCallback!(val: T): void;

В этом случае строгий тип val не важен и может быть принесен в жертву. Поскольку barCallback нельзя заменить сигнатурой метода в классе, слияние интерфейсов кажется способом подавить ошибку без дальнейшего ослабления типов:

interface Foo<T> {
  barCallback(val: T): void;
}

Существуют ли другие возможные обходные пути в случаях, подобных этому?

Я был бы признателен за объяснение, почему именно val: T в типах функций влияет на тип класса таким образом.

1 Ответ

6 голосов
/ 09 мая 2020

Это основная проблема дисперсии. Итак, сначала праймер дисперсии:

О дисперсии

Учитывая общий c тип Foo<T> и два связанных типа Animal и Dog extends Animal. Существует четыре возможных отношения между Foo<Animal> и Foo<Dog>:

  1. Ковариация - стрелка наследования указывает в том же направлении для Foo<Animal> и Foo<Dog> как это работает для Animal и Dog, поэтому Foo<Dog> является подтипом Foo<Animal>, что также означает, что Foo<Dog> присваивается Foo<Animal>
type CoVariant<T> = () => T
declare var coAnimal: CoVariant<Animal>
declare var coDog: CoVariant<Dog>
coDog = coAnimal; // ?
coAnimal = coDog; // ✅
Контравариантность - стрелка наследования указывает в противоположном направлении для Foo<Animal> и Foo<Dog>, как и для Animal и Dog, поэтому Foo<Animal> на самом деле является подтипом Foo<Dog>, что также означает, что Foo<Animal> можно присвоить Foo<Dog>
type ContraVariant<T> = (p: T) => void
declare var contraAnimal: ContraVariant<Animal>
declare var contraDog: ContraVariant<Dog>
contraDog = contraAnimal; // ✅
contraAnimal = contraDog; // ?
Инвариантность - хотя Dog и Animal связаны, Foo<Animal> и Foo<Dog> не имеют между собой никаких отношений, поэтому ни один из них не может быть назначен другому.
type InVariant<T> = (p: T) => T
declare var inAnimal: InVariant<Animal>
declare var inDog: InVariant<Dog>
inDog = inAnimal; // ?
inAnimal = inDog; // ?
Двувариантность - если Dog и Animal связаны, оба Foo<Animal> являются подтипом Foo<Dog>, а Foo<Animal> - подтипом Foo<Dog>, что означает, что любой тип может быть назначен другому. В более строгой системе типов это был бы патологический случай, когда T может фактически не использоваться, но в машинописном тексте позиции параметров методов считаются двухвариантными.

class BiVariant<T> { m(p: T): void {} }
declare var biAnimal: BiVariant<Animal>
declare var biDog: BiVariant<Dog>
biDog = biAnimal; // ✅
biAnimal = biDog; // ✅

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

Итак, вопрос в том, как использование T влияет на дисперсию? В машинописном тексте положение параметра типа определяет дисперсию, некоторые примеры:

  1. Co-varaint - T используется в качестве поля или как возвращаемый тип функции
  2. Contra-varaint - T используется как параметр функции подпись под strictFunctionTypes
  3. Инвариант - T используется как в ковариантной, так и в контравариантной позиции
  4. Двунаправленный - T используется как параметр определения метода в strictFunctionTypes или как тип параметра любого метода или функции, если strictFunctionTypes выключены.

Причина различного поведения параметров методов и функций в strictFunctionTypes объясняется здесь :

Более строгая проверка применяется ко всем типам функций, кроме тех происходящие из объявлений метода или конструктора. Методы исключены специально, чтобы гарантировать, что общие классы и интерфейсы (например, Array) в основном связаны ковариантно. Влияние методов строгой проверки будет гораздо более серьезным изменением, поскольку большое количество общих c типов станет инвариантным (даже в этом случае мы можем продолжить изучение этого более строгого режима).

Назад на вопрос

Итак, давайте посмотрим, как использование T влияет на дисперсию Foo.

  • barCallback!: (val: T) => void; - используется как параметр в члене это функция -> противоположная позиция

  • baz(callback: ((val: T) => void)): void - используется как параметр в параметре обратного вызова другой функции. Это немного сложно, предупреждение о спойлере, оно окажется ковариантным. Давайте рассмотрим этот упрощенный пример:

type FunctionWithCallback<T> = (cb: (a: T) => void) => void

// FunctionWithCallback<Dog> can be assigned to FunctionWithCallback<Animal>
let withDogCb: FunctionWithCallback<Dog> = cb=> cb(new Dog());
let aliasDogCbAsAnimalCb: FunctionWithCallback<Animal> = withDogCb; // ✅
aliasDogCbAsAnimalCb(a => a.animal) // the cb here is getting a dog at runtime, which is fine as it will only access animal members


let withAnimalCb: FunctionWithCallback<Animal> = cb => cb(new Animal());
// FunctionWithCallback<Animal> can NOT be assigned to FunctionWithCallback<Dog>
let aliasAnimalCbAsDogCb: FunctionWithCallback<Dog> = withAnimalCb; // ?
aliasAnimalCbAsDogCb(d => d.dog) // the cb here is getting an animal at runtime, which is bad, since it is using `Dog` members

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

В первом примере обратный вызов, который мы передаем на aliasDogCbAsAnimalCb, ожидает получить Animal, поэтому используются только члены Animal. Реализация withDogCb создаст Dog и передаст его функции обратного вызова, но это нормально. Обратный вызов будет работать так, как ожидалось, используя только те свойства базового класса, которые он ожидает.

Во втором примере обратный вызов, который мы передаем в aliasAnimalCbAsDogCb, ожидает получить Dog, поэтому он использует Dog члены. Но реализация withAnimalCb передаст в обратный вызов экземпляр животного. Это может привести к ошибкам времени выполнения, поскольку обратный вызов заканчивается использованием членов, которых нет.

Таким образом, учитывая, что присвоение FunctionWithCallback<Dog> FunctionWithCallback<Animal> безопасно, мы приходим к выводу, что такое использование T определяет ковариацию.

Заключение

Итак, у нас есть T, используемое как в ковариантной, так и в контравариантной позиции в Foo, это означает, что Foo инвариантно в T. Это означает, что Foo<any[] | { [s: string]: any }> и Foo<any[]> на самом деле не связаны между собой типами с точки зрения системы типов. И хотя перегрузки более свободны в своих проверках, они ожидают, что возвращаемый тип перегрузки и реализация будут связаны (либо возврат реализации, либо возврат перегрузки должен быть подтипом другого, ex )

Почему некоторые изменения заставляют его работать:

  • Выключение strictFunctionTypes сделает сайт barCallback для T двувариантным, поэтому Foo будет ковариантным
  • Преобразование barCallback в метод, делает сайт для T бивариантным, поэтому Foo будет ковариантным
  • Удаление barCallback удалит контравариантное использование, и поэтому Foo будет ковариантным
  • Удаление baz удалит ковариантное использование T, сделав Foo контравариантным в T.

Обходные пути

Вы можете оставить strictFunctionTypes на и вырезать исключение только для этого одного обратного вызова, чтобы он оставался двухвариантным, с помощью бивариантного хака (объяснено здесь для более узкого варианта использования, но применяется тот же принцип):


type BivariantCallback<C extends (... a: any[]) => any> = { bivarianceHack(...val: Parameters<C>): ReturnType<C> }["bivarianceHack"];


class Foo<T> {
    static manyFoo(): Foo<any[] | { [s: string]: any }>;
    static manyFoo(): Foo<any[]> {
        return ['stub'] as any;
    }

    barCallback!: BivariantCallback<(val: T) => void>;

    constructor() {
        // get synchronously from elsewhere
        (callback => {
            this.barCallback = callback;
        })((v: any) => {});
    }

    baz(callback: ((val: T) => void)): void {}
}

Детская площадка Ссылка

...