Typescript, как разделить связанную вспомогательную функцию между двумя классами, которые добавляют элементы на карту - PullRequest
0 голосов
/ 04 ноября 2019

Допустим, у меня есть два класса, и я бы хотел, чтобы они разделили служебную функцию между ними. Этот служебный класс обращается к свойству, которое является картой типа объекта. Тип объекта, хранящийся на карте, отличается для каждого класса. Функция вызывается в ограниченном экземпляре вызывающего ее класса и передается в качестве аргумента объекта, который она должна добавить в карту. Упрощенная версия может выглядеть следующим образом.

interface fooItem {
    name: string
}

interface barItem {
    name: string
    compact: boolean
}

function setItems(this: Foo | Bar, item: fooItem | barItem) {
    // Error here on 'item' arg
    this.items.set(item.name, item); 
}

class Foo {
    constructor() {
        this.items = new Map();
    }
    items: Map<string, fooItem>;
    setItems = setItems.bind(this);
}

class Bar {
    constructor() {
        this.items = new Map();
    }
    items: Map<string, barItem>;
    setItems = setItems.bind(this);
}

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

Этот код вызывает ошибку компилятора

Argument of type 'fooItem | barItem' is not assignable to parameter of type 'barItem'.
  Property 'compact' is missing in type 'fooItem' but required in type 'barItem'.

Предполагается, что объект будет когда-либо использоваться только с классом, с которым он связан, но я понимаю, как компилятор не может гарантировать их сопряжение.

Каков правильный синтаксис синтаксиса, чтобы компилятор знал, что элемент будет иметь тип fooItem, если это тип Foo, и наоборот?

Ответы [ 2 ]

0 голосов
/ 04 ноября 2019

Чтобы достичь требуемого ограничения, я бы сделал универсальную функцию, подобную следующей:

function setItems<T extends { name: string }>(
    this: { items: Map<string, T> },
    item: T
) {
    this.items.set(item.name, item); // okay
}

Но вы найдете, если попытаетесь bind() до this, компилятор забудет, что item уже {name: string}, и вы получите что-то слишком слабо набранное, чтобы быть полезным само по себе.

Затем вам нужно будет вручнуюрасширить setItems до конкретного типа для каждого класса:

// separate var with annotation
const fooSetItems: (this: Foo, item: fooItem) => void = setItems;
class Foo {
    constructor() {
        this.items = new Map();
    }
    items: Map<string, fooItem>;
    setItems = fooSetItems.bind(this);
}


class Bar {
    constructor() {
        this.items = new Map();
    }
    items: Map<string, barItem>;

    // same var with assertion
    setItems = (setItems as (this: Bar, item: barItem) => void).bind(this);
}

Это работает, но сложнее, чем мне нравится.


Предложение вместо этого: так как вы ужеПрикрепляя функционально-значимые свойства к каждому экземпляру Foo и Bar вместо использования прототипа, я бы полностью отказался от this параметров и просто использовал бы простую функцию:

const bindSetItems =
    <N extends { name: string }>(ctx: { items: Map<string, N> }) =>
        (item: N) => ctx.items.set(item.name, item);

class Foo {
    constructor() {
        this.items = new Map();
    }
    items: Map<string, fooItem>;
    setItems = bindSetItems(this);
}

class Bar {
    constructor() {
        this.items = new Map();
    }
    items: Map<string, barItem>;
    setItems = bindSetItems(this);
}

Этоведет себя так же, но вместо того, чтобы возиться с this, он просто использует карри вместо прототипического наследования для выполнения работы.


Надеюсь, одна из тех работ для вас. Удачи!

Ссылка на код

0 голосов
/ 04 ноября 2019

Было бы лучше, если бы вы использовали явную функцию привязки вместо набора bind, например:

interface NamedItem {
  name: string
}

interface HasNamedMap<T extends NamedItem> {
  items: Map<string, T>;
}

function setItems<T extends NamedItem>(obj: HasNamedMap<T>): (item: T) => void {
  return function(item: T) {
    obj.items.set(item.name, item);
  }
}

Таким образом, определенные типы "элементов" могут быть правильно выведены:

class Foo {
  items: Map<string, fooItem> = new Map();
  setItems = setItems(this); // inferred as (item: fooItem) => void
}

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

...