Как мы можем напечатать фабрику классов, которая генерирует класс с заданным литералом объекта?
Например, я сделал библиотеку JavaScript под названием lowclass
и мне интересно, как заставить его работать в системе типов TypeScript.
Библиотека позволяет нам определить класс путем передачи объекта-литерала в API, как показано ниже, и мне интересно, как заставить его возвращать тип, который фактически совпадает с написанием обычного class {}
:
import Class from 'lowclass'
const Animal = Class('Animal', {
constructor( sound ) {
this.sound = sound
},
makeSound() { console.log( 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"
Как видите, Class()
а также Class().extends()
API принимает объектные литералы, используемые для определения классов.
Как я могу набрать этот API, чтобы конечный результат состоял в том, что Animal
а также Dog
ведут себя в TypeScript, как если бы я написал их, используя родной class Animal {}
а также class Dog extends Animal {}
синтаксис?
То есть, если бы я переключил кодовую базу с JavaScript на TypeScript, как бы я мог набрать API в этом случае, чтобы конечный результат состоял в том, что люди, использующие мои классы, созданные с использованием низкоуровневого класса, могут использовать их как обычные классы?
РЕДАКТИРОВАТЬ1: кажется, простой способ набирать классы, которые я делаю с низкими классами, написав их на JavaScript и объявив регулярные class {}
определения внутри .d.ts
файлы определений типов. Кажется более сложным, если вообще возможно, преобразовать мою кодовую базу низкого класса в TypeScript, чтобы она могла автоматически печатать при определении классов, а не делать .d.ts
файлы для каждого класса.
РЕДАКТИРОВАТЬ 2: Другая идея, которая приходит в голову, состоит в том, что я могу оставить низкий класс как есть (JavaScript напечатан как any
), тогда, когда я определяю классы, я могу просто определить их, используя as SomeType
где SomeType
может быть объявлением типа прямо в том же файле. Это может быть менее СУХОЕ, чем сделать lowclass библиотекой TypeScript, чтобы типы были автоматическими, так как мне пришлось бы повторно объявлять методы и свойства, которые я уже определил при использовании API низшего класса.
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.log(this.sound) // this typed correctly }
})
Этот тип в методах
в Animal
декларация выше, this
правильно набирается в методах типа, это хорошо и отлично работает для литералов объекта. Для литералов объекта this
будет иметь тип текущего объекта в функциях, определенных в литерале объекта. Проблема в том, что нам нужно указать тип this
при расширении существующего типа, как this
будет иметь члены текущего литерала объекта плюс члены базового типа. К счастью, машинопись позволяет нам делать это, используя ThisType<T>
тип маркера, используемый компилятором и описанный здесь
Создание расширяет
Теперь, используя контекстное это, мы можем создать 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.log(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.log(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();
Надеюсь, это поможет, дайте мне знать, если я могу помочь с чем-нибудь еще:)