Как правильно расширять классы через generic-интерфейсы

У меня есть условный базовый класс с реализацией:

interface IMessageStruct<T> {
    name: keyof T;
    type: number;
}

abstract class Base<T> {
    protected abstract readonly types: Array<IMessageStruct<T>>;

    toJSON(): T {
        return (this.types.reduce((memo, { name }) => {
            memo[name] = this[name];
            return memo;
        }, {} as Partial<T>)) as T;
    }

    // other code
}

Есть множество дочерних классов, которые выглядят следующим образом

class Init extends Base<IInit> implements IInit {
    // some code
}

Подскажите, можно ли как-то переписать Base класс таким образом, чтобы в каждом дочернем не было необходимости наследоваться от интерфейса и в методе toJSON не выводилась ошибка на this[name]

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


Ответы (1 шт):

Автор решения: qwabra

как не писать implements у класса?

назвать интерфейс так же как и класс (смотри declaration-merging )

interface $type<T> {
    name: keyof T;
    type: number;
}

type $typeList<T> = $type<T>[];

abstract class Base {
    protected abstract readonly types: $typeList<this>;
    toJSON(): Omit<this, 'toJSON' | 'types'> { return null as any }
}

// ------------

interface Next {
    name: string;
    surname: string
    age: number
}
class Next extends Base {
    types = [
        Next.genType('name', 1),
        Next.genType('age', 1),
        Next.genType('surname', 1),
    ]
}
namespace Next {
    type $genType<T> = (name: keyof T, type: number) => $type<T>
    type $exclude = 'toJSON' | 'types'
    export const genType: $genType<Omit<Next, $exclude>> = (name, type) => ({ name, type })
}

const next = new Next
const json = next.toJSON() // Pick<Next, "name" | "surname" | "age">

если брать ваш код, то можно написать так:

interface IMessageStruct<T> {
    name: keyof T;
    type: number;
}

abstract class Base<T> {
    protected abstract readonly types: Array<IMessageStruct<T>>;
    toJSON(): T { return null as any}
}

// ------------

interface Next {
    name: string;
    surname: string
    age: number
}
class Next extends Base<Next> {
    types = [
        Next.genType('name', 1),
        Next.genType('surname', 1),
    ]
}
namespace Next {
    type $genType<T> = (name: keyof T, type: number) => IMessageStruct<T>
    export const genType: $genType<Next> = (name, type) => ({ name, type })
}

рекомендую подумать над таким вариантом

разделить данные и методы (например age и setAge)
то есть все данные вынести в переменную, например data

interface $type<T> {
    name: keyof T;
    type: number;
}
type $typeList<T> = $type<T>[];
abstract class Base {
    abstract data
    protected abstract readonly types: $typeList<this['data']>;
    toJSON(): this['data'] { return null as any }
}
// ------------

interface $data {
    name: string;
    surname: string
    age: number
}
class Next extends Base {
    readonly data: $data
    types = [
        Next.genType('name', 1),
        Next.genType('age', 1),
        Next.genType('surname', 1),
    ]
    setAge(age: number) { this.data.age = age }
}
namespace Next {
    type $genType<T> = (name: keyof T, type: number) => $type<T>
    export const genType: $genType<Next['data']> = (name, type) => ({ name, type })
}

const next = new Next
next.setAge(22)
const json = next.toJSON() // Pick<Next, "name" | "surname" | "age">
→ Ссылка