Как мы можем ввести фабрику классов, которая генерирует класс с учетом литерала объекта?

Например, я создал библиотеку JavaScript под названием lowclass, и мне интересно, как заставить ее работать в системе типов TypeScript.

Библиотека позволяет нам определять класс, передавая объектный литерал в API, как показано ниже, и мне интересно, как заставить его возвращать тип, который фактически совпадает с записью обычного class {}:

import Class from 'lowclass'

const Animal = Class('Animal', {
  constructor( sound ) {
    this.sound = sound
  },
  makeSound() { console.info( this.sound ) }
})

const Dog = Class('Dog').extends(Animal, ({Super}) => ({
  constructor( size ) {
    if ( size === 'small' )
      Super(this).constructor('woof')
    if ( size === 'big' )
      Super(this).constructor('WOOF')
  },
  bark() { this.makeSound() }
}))

const smallDog = new Dog('small')
smallDog.bark() // "woof"

const bigDog = new Dog('big')
bigDog.bark() // "WOOF"

Как видите, API Class() и Class().extends() принимают объектные литералы, используемые для определения классов.

Как я могу ввести этот API, чтобы в конечном результате Animal и Dog вели себя в TypeScript, как если бы я написал их с использованием собственного синтаксиса class Animal {} и class Dog extends Animal {}?

То есть, если бы мне пришлось переключить базу кода с JavaScript на TypeScript, как я мог бы ввести API в этом случае, чтобы конечный результат состоял в том, что люди, использующие мои классы, созданные с помощью lowclass, могли использовать их как обычные классы?

РЕДАКТИРОВАТЬ1: Кажется, это простой способ ввести классы, которые я создаю с помощью lowclass, написав их в JavaScript и объявив обычные определения class {} внутри файлов определений типов .d.ts. Кажется более сложным, если даже возможно, преобразовать мою базу кода низкого класса в TypeScript, чтобы он мог автоматизировать набор текста при определении классов, а не создавать файлы .d.ts для каждого класса.

EDIT2: Еще одна идея, которая приходит на ум, заключается в том, что я могу оставить lowclass как есть (JavaScript, набранный как any), а затем, когда я определяю классы, я могу просто определять их с помощью as SomeType, где SomeType может быть объявлением типа прямо внутри того же файла. Это могло бы быть менее СУХОЕ, чем сделать lowclass библиотекой TypeScript, чтобы типы были автоматическими, поскольку мне пришлось бы повторно объявлять методы и свойства, которые я уже определил при использовании lowclass API.

Уверен, вы сможете сделать его очень СУХИМ с помощью универсального. Что-то вроде function Class<T extends object>(name: string, definition: T): ActualClass<T>. Сложная часть - это аргументы конструктора. Возможно, вам придется повторить это.

Lazar Ljubenović 17.06.2018 20:47

Что касается расширения, я считаю его слишком динамичным и хакерским. TypeScript и метапрограммирование несовместимы друг с другом.

Lazar Ljubenović 17.06.2018 20:48

Я думаю, что мы сможем получить приличный набор текста, лучше, чем любой другой, или повторно объявить все типы. Не уверен, доберусь ли я до этого сегодня вечером, но если никто другой до этого не дойдет, я отвечу на него утром :-)

Titian Cernicova-Dragomir 17.06.2018 21:42
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
1
3
187
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Хорошо, поэтому нам нужно исправить несколько проблем, чтобы это работало аналогично классам Typescript. Прежде чем мы начнем, я выполняю все приведенное ниже кодирование в режиме Typescript strict. Некоторое поведение ввода не будет работать без него, мы можем определить конкретные необходимые параметры, если вы заинтересованы в решении.

Тип и значение

В машинописном тексте классы занимают особое место, поскольку они представляют как значение (функция-конструктор - это значение Javascript), так и тип. const, который вы определяете, представляет только значение (конструктор). Например, чтобы иметь тип для Dog, нам нужно явно определить тип экземпляра Dog, чтобы его можно было использовать позже:

const Dog =  /* ... */
type Dog = InstanceType<typeof Dog>
const smallDog: Dog = new Dog('small') // We can now type a variable or a field

Функция для конструктора

Вторая проблема заключается в том, что constructor - это простая функция, а не функция-конструктор, и машинописный текст не позволит нам вызвать new для простой функции (по крайней мере, не в строгом режиме). Чтобы исправить это, мы можем использовать условный тип для сопоставления между конструктором и исходной функцией. Подход аналогичен здесь, но я собираюсь написать его для нескольких параметров, чтобы упростить задачу, вы можете добавить больше:

type IsValidArg<T> = T extends object ? keyof T extends never ? false : true : true;

type FunctionToConstructor<T, TReturn> =
    T extends (a: infer A, b: infer B) => void ?
        IsValidArg<B> extends true ? new (p1: A, p2: B) => TReturn :
        IsValidArg<A> extends true ? new (p1: A) => TReturn :
        new () => TReturn :
    never;

Создание типа

С помощью указанного выше типа мы теперь можем создать простую функцию Class, которая будет принимать литерал объекта и строить тип, похожий на объявленный класс. Если здесь нет поля constructor, мы будем использовать пустой конструктор, и мы должны удалить constructor из типа, возвращаемого функцией нового конструктора, которую мы вернем, мы можем сделать это с помощью Pick<T, Exclude<keyof T, 'constructor'>>. Мы также сохраним поле __original, чтобы иметь исходный тип литерала объекта, который будет полезен позже:

function Class<T>(name: string, members: T): FunctionToConstructor<ConstructorOrDefault<T>, Pick<T, Exclude<keyof T, 'constructor'>>> & { __original: T  }


const Animal = Class('Animal', {
    sound: '', // class field
    constructor(sound: string) {
        this.sound = sound;
    },
    makeSound() { console.info(this.sound) // this typed correctly }
})

Этот тип в методах

В приведенном выше объявлении Animalthis введен правильно в методах типа, это хорошо и отлично подходит для объектных литералов. Для объектных литералов this будет иметь тип текущего объекта в функциях, определенных в объектном литерале. Проблема в том, что нам нужно указать тип this при расширении существующего типа, поскольку this будет иметь члены текущего литерала объекта плюс члены базового типа. К счастью, машинописный текст позволяет нам сделать это, используя ThisType<T>, тип маркера, используемый компилятором и описанный здесь.

Создание расширений

Теперь, используя контекстный this, мы можем создать функциональность extends, единственная проблема, которую нужно решить, - это посмотреть, есть ли у производного класса собственный конструктор, или мы можем использовать базовый конструктор, заменив тип экземпляра новым типом.

type ReplaceCtorReturn<T, TReturn> =
    T extends new (a: infer A, b: infer B) => void ?
        IsValidArg<B> extends true ? new (p1: A, p2: B) => TReturn :
        IsValidArg<A> extends true ? new (p1: A) => TReturn :
        new () => TReturn :
    never;
function Class(name: string): {
    extends<TBase extends {
        new(...args: any[]): any,
        __original: any
    }, T>(base: TBase, members: (b: { Super : (t: any) => TBase['__original'] }) => T & ThisType<T & InstanceType<TBase>>):
        T extends { constructor: infer TCtor } ?
        FunctionToConstructor<ConstructorOrDefault<T>, InstanceType<TBase> & Pick<T, Exclude<keyof T, 'constructor'>>>
        :
        ReplaceCtorReturn<TBase, InstanceType<TBase> & Pick<T, Exclude<keyof T, 'constructor'>>>
}

Собираем все вместе:

type IsValidArg<T> = T extends object ? keyof T extends never ? false : true : true;

type FunctionToConstructor<T, TReturn> =
    T extends (a: infer A, b: infer B) => void ?
    IsValidArg<B> extends true ? new (p1: A, p2: B) => TReturn :
    IsValidArg<A> extends true ? new (p1: A) => TReturn :
    new () => TReturn :
    never;

type ReplaceCtorReturn<T, TReturn> =
    T extends new (a: infer A, b: infer B) => void ?
    IsValidArg<B> extends true ? new (p1: A, p2: B) => TReturn :
    IsValidArg<A> extends true ? new (p1: A) => TReturn :
    new () => TReturn :
    never;

type ConstructorOrDefault<T> = T extends { constructor: infer TCtor } ? TCtor : () => void;

function Class(name: string): {
    extends<TBase extends {
        new(...args: any[]): any,
        __original: any
    }, T>(base: TBase, members: (b: { Super: (t: any) => TBase['__original'] }) => T & ThisType<T & InstanceType<TBase>>):
        T extends { constructor: infer TCtor } ?
        FunctionToConstructor<ConstructorOrDefault<T>, InstanceType<TBase> & Pick<T, Exclude<keyof T, 'constructor'>>>
        :
        ReplaceCtorReturn<TBase, InstanceType<TBase> & Pick<T, Exclude<keyof T, 'constructor'>>>
}
function Class<T>(name: string, members: T & ThisType<T>): FunctionToConstructor<ConstructorOrDefault<T>, Pick<T, Exclude<keyof T, 'constructor'>>> & { __original: T }
function Class(): any {
    return null as any;
}

const Animal = Class('Animal', {
    sound: '',
    constructor(sound: string) {
        this.sound = sound;
    },
    makeSound() { console.info(this.sound) }
})

new Animal('').makeSound();

const Dog = Class('Dog').extends(Animal, ({ Super }) => ({
    constructor(size: 'small' | 'big') {
        if (size === 'small')
            Super(this).constructor('woof')
        if (size === 'big')
            Super(this).constructor('WOOF')
    },

    makeSound(d: number) { console.info(this.sound) },
    bark() { this.makeSound() },
    other() {
        this.bark();
    }
}))
type Dog = InstanceType<typeof Dog>

const smallDog: Dog = new Dog('small')
smallDog.bark() // "woof"

const bigDog = new Dog('big')
bigDog.bark() // "WOOF"

bigDog.bark();
bigDog.makeSound();

Надеюсь, это поможет, дайте мне знать, если я могу помочь еще чем-нибудь :)

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

Боже мой, это совершенно другая форма программирования (например, программирование типов). Спасибо за это понимание. Вопросов пока нет. Надо изучить этот метатипинг.

trusktr 18.06.2018 23:06

@trusktr Ага, это еще один способ программирования :) Интересный факт, система типов машинописного текста - это Тьюринг, завершенный сам по себе github.com/Microsoft/TypeScript/issues/14833

Titian Cernicova-Dragomir 19.06.2018 06:55

Привет, Тициан, я учился на своих других вопросах (спасибо за другие ответы!), И теперь я начинаю лучше понимать все это. Настоящая проблема немного сложнее. Вот более полная картина того, как API Class() выглядит на простом JS: trusktr.io:7777/ebisimokak.js. Как вы можете видеть, есть также статические, частные и защищенные члены, которые определены в подобъектах. Можно ли ввести эти частные / защищенные / статические члены как таковые?

trusktr 02.01.2019 08:08

Должен сказать, это впечатляет! Обратной стороной этого по сравнению с обычными классами является то, что при наведении курсора на переменную (например, bigDog) нелегко понять всплывающую подсказку. С обычными классами мы увидим const bigDog: Dog во всплывающей подсказке, но в примере с игровой площадкой всплывающая подсказка показывает: const bigDog: Pick<{ sound: string; constructor(sound: string): void; makeSound(): void; }, "sound" | "makeSound"> & Pick<{ constructor(size: "small" | "big"): void; makeSound(d: number): void; bark(): void; other(): void; }, "makeSound" | ... 1 more ... | "other">. Я думаю, что нет никакого способа обойти это?

trusktr 02.01.2019 08:19

@trusktr Я думаю, что мы можем улучшить вещи даже на стороне всплывающих подсказок .. Статика и рядовые на первый взгляд кажутся выполнимыми (я только что проснулся, так что относитесь к этому с недоверием: P). Свяжитесь со мной через gitter, сложно вести беседу в комментариях, и, вероятно, потребуется много времени, чтобы разобраться в этом вопросе ...

Titian Cernicova-Dragomir 02.01.2019 08:24

Другие вопросы по теме