Например, я создал библиотеку 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.
Что касается расширения, я считаю его слишком динамичным и хакерским. TypeScript и метапрограммирование несовместимы друг с другом.
Я думаю, что мы сможем получить приличный набор текста, лучше, чем любой другой, или повторно объявить все типы. Не уверен, доберусь ли я до этого сегодня вечером, но если никто другой до этого не дойдет, я отвечу на него утром :-)



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Хорошо, поэтому нам нужно исправить несколько проблем, чтобы это работало аналогично классам 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 Ага, это еще один способ программирования :) Интересный факт, система типов машинописного текста - это Тьюринг, завершенный сам по себе github.com/Microsoft/TypeScript/issues/14833
Привет, Тициан, я учился на своих других вопросах (спасибо за другие ответы!), И теперь я начинаю лучше понимать все это. Настоящая проблема немного сложнее. Вот более полная картина того, как API Class() выглядит на простом JS: trusktr.io:7777/ebisimokak.js. Как вы можете видеть, есть также статические, частные и защищенные члены, которые определены в подобъектах. Можно ли ввести эти частные / защищенные / статические члены как таковые?
Должен сказать, это впечатляет! Обратной стороной этого по сравнению с обычными классами является то, что при наведении курсора на переменную (например, 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 Я думаю, что мы можем улучшить вещи даже на стороне всплывающих подсказок .. Статика и рядовые на первый взгляд кажутся выполнимыми (я только что проснулся, так что относитесь к этому с недоверием: P). Свяжитесь со мной через gitter, сложно вести беседу в комментариях, и, вероятно, потребуется много времени, чтобы разобраться в этом вопросе ...
Уверен, вы сможете сделать его очень СУХИМ с помощью универсального. Что-то вроде
function Class<T extends object>(name: string, definition: T): ActualClass<T>. Сложная часть - это аргументы конструктора. Возможно, вам придется повторить это.