Задать вопрос
Demonov
@Demonov
Frontend developer

TypeScript. Как затипизировать this?

Привет.
У меня есть:
  • 1 родительский класс
  • 3 класса компонента, каждый из которых расширят родительский

Компоненты при инициализации передают родительскому классу массив имен своих методов, которые нужно повесить как обработчик на документ.
Родительский класс пробегается по массиву и навешивает их на документ)
5ede95b48cab2627710736.png

Как в этом случае правильно указать тип this?
В родительском классе я делаю что-то типа этого:
this.listeners.forEach((listener) => {
    const method: string = getMethodName(listener);

    if (method in this) {
      this[method] = this[method].bind(this);
      this.$root.addEventListener(listener, this[method]);
    }
});

Ошибка у this[method]
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Listener'.   No index signature with a parameter of type 'string' was found on type 'Listener'.


Я знаю TS не понимает, что именно это за строка, но не знаю как это можно решить.
any не применяю, в tsconfig стоит strict: true

Я не против если вы предложите другой подход
  • Вопрос задан
  • 903 просмотра
Подписаться 2 Средний 1 комментарий
Решения вопроса 2
profesor08
@profesor08
Задача, мягко говоря, так себе. Много подводных камней и тд. Однако можно реализовать с оговорками. Методы onClick, onInput и тд., описаны в специальном интерфейсе, его можно имплементировать, или как-то использовать. Но, во первых, тебе придется играться с этими наименованиями, чтоб как-то избежать переопределения этих свойств из разных объектов. Во вторых, это делать плохо и ненадежно. Для всего этого придумали addEventListener. По этому, можно посмотреть какой у него интерфейс, и на основе него построить свои классы так, чтоб можно было создавать какие угодно классы, с нужными ивентами. Но в них не должно быть ничего лишнего, кроме ивентов.

TypeScript Playground

interface Object {
  entries<T>(o: { [s: string]: T } | ArrayLike<T>): [string, T][];
}

type TEventType = keyof HTMLElementEventMap;

type TEventCallback = (this: HTMLElement, ev: HTMLElementEventMap[TEventType]) => any;

type IListener = {
  [key in TEventType]?: TEventCallback;
};

class ListenerA implements IListener {
  public click() {
    console.log("ListenerA.click");
  }

  public mousemove() {
    console.log("ListenerA.mousemove");
  }
}

class ListenerB implements IListener {
  public click() {
    console.log("ListenerB.click");
  }
}

class DocumentListener {
  constructor(...components: IListener[]) {
    for (const component of components) {
      const proto = Reflect.getPrototypeOf(component);
      const events = Reflect.ownKeys(proto)
        .filter(key => key !== "contructor") as (keyof IListener)[];

      for (const event of events) {
        const callback = component[event];
        if (callback !== undefined) {
          document.body.addEventListener(event, callback);
        }
      }
    }
  }
}

const dl = new DocumentListener(new ListenerA(), new ListenerB());


P.S. Крайне не рекомендую заниматься такой фигней. Есть более удобные способы навешивания событий, при том понятные, легко-поддерживаемые.

type TEventType = keyof HTMLElementEventMap;

type TEventCallback = (this: HTMLElement, ev: HTMLElementEventMap[TEventType]) => any;

type IEvent = {
  event: TEventType;
  callback: TEventCallback;
}

interface IListener {
  events: IEvent[];
  on(event: TEventType, callback: TEventCallback): any;
};

class Listener implements IListener {
  events: IEvent[] = [];

  on(event: TEventType, callback: TEventCallback) {
    this.events.push({event, callback});
  }
}

class DocumentListener {
  constructor(...components: IListener[]) {
    for (const component of components) {
      for (const { event, callback } of component.events) {
        document.body.addEventListener(event, callback);
      }
    }
  }
}

const l1 = new Listener();

l1.on("click", () => {
  console.log("l1.click");
});

const l2 = new Listener();

l2.on("click", () => {
  console.log("l2.click");
});

const dl = new DocumentListener(l1, l2);
Ответ написан
Комментировать
@forspamonly2
надо не this типизировать, а имя метода вместо string делать keyof this.

abstract class BaseComp {
    abstract listeners: readonly (keyof this)[];
    attachListeners() {
        this.listeners.forEach(name => {
            const method = this[name];
            if(typeof method == 'function') {
                this[name] = method.bind(this);
                // attach
            }
        })
    }
    onError() {}
}
class Comp extends BaseComp {
    listeners = ["onClick", "onError"] as const;
    onClick(){}
}


правда так оно позволит вместо имени метода имя проперти подсунуть. а с фильтром на условном типе отсюда придётся тип класса потомка передавать явно (выводить оно его не хочет). но зато кроме имён методов ничего передать будет нельзя:

type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T]; 
function attachListeners<T extends BaseComp2>(component:T, listeners: FunctionPropertyNames<T>[]) {
    listeners.forEach(name => {
        const method = component[name];
        if(typeof method == 'function') {
            component[name] = method.bind(component);
            // attach
        }
    })
}

abstract class BaseComp2 {
    onError() {}
}

class Comp2 extends BaseComp2 {
    constructor() {
        super();
        attachListeners<Comp2>(this,["onCLick", "onError"]);
    }
    onCLick(){}
}
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 1
@juxifo
Что-что, простите?
this[method] = this[method].bind(this);
Не смущает, нет? У вас в this[method] и так область видимости — this.
this.$root.addEventListener(listener, () => this[method]());

Проверьте тип this[method], если не сработает.
Сама ваша реализация — огромный костыль, но нужно смотреть весь код. Навскидку, лучше было бы сделать публичный метод (псевдокод):
public void on(string event, ((Event event) => void) callback)

И его дергать, наследование тут смотрится ужасно.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы