Javascript 在给定对象文本的情况下,如何键入生成类的类工厂?

Javascript 在给定对象文本的情况下,如何键入生成类的类工厂?,javascript,typescript,types,Javascript,Typescript,Types,例如,我创建了一个名为的JavaScript库,我想知道如何使它在TypeScript类型系统中工作 该库允许我们通过将对象文本传递到API中来定义一个类,如下所示,我想知道如何使它返回一个与编写常规类{}相同的类型: import Class from 'lowclass' const Animal = Class('Animal', { constructor( sound ) { this.sound = sound }, makeSound() { console.

例如,我创建了一个名为的JavaScript库,我想知道如何使它在TypeScript类型系统中工作

该库允许我们通过将对象文本传递到API中来定义一个类,如下所示,我想知道如何使它返回一个与编写常规
类{}
相同的类型:

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 extensed Animal{}
语法编写它们一样

也就是说,如果我要将代码库从JavaScript切换到TypeScript,那么在这种情况下,我应该如何键入API,以便最终结果是使用我用lowclass创建的类的人可以像使用常规类一样使用它们

EDIT1:用JavaScript编写我用lowclass创建的类,并在
.d.ts
类型定义文件中声明常规的
类{}
定义,这似乎是一种键入类的简单方法。如果可能的话,将我的低级代码库转换为TypeScript似乎更难,这样它就可以在定义类时自动键入,而不是为每个类生成
.d.ts
文件


EDIT2:我想到的另一个想法是,我可以让lowclass保持原样(JavaScript类型为
any
),然后当我定义类时,我可以使用
作为SomeType
来定义它们,其中
SomeType
可以是同一文件中的类型声明。这可能比使lowclass成为一个TypeScript库以便类型是自动的要简单,因为我必须重新声明在使用lowclass API时已经定义的方法和属性。

好的,因此我们需要解决几个问题,以便以类似于TypeScript类的方式工作。在我们开始之前,我在Typescript
strict
模式下完成下面的所有编码,没有它,某些键入行为将无法工作,如果您对解决方案感兴趣,我们可以确定所需的特定选项

类型和价值 在typescript中,类占有特殊的地位,因为它们同时表示一个值(构造函数是一个Javascript值)和一个类型。您定义的
常量
仅表示值(构造函数)。要获得
Dog
的类型,例如,我们需要明确定义
Dog
的实例类型,以便以后可以使用:

const Dog =  /* ... */
type Dog = InstanceType<typeof Dog>
const smallDog: Dog = new Dog('small') // We can now type a variable or a field
建筑类型 使用上面的类型,我们现在可以创建简单的
Class
函数,该函数将接受对象文本并构建一个类似于声明类的类型。如果这里没有
constructor
字段,我们将创建一个空构造函数,并且我们必须从将返回的新构造函数返回的类型中删除
constructor
,我们可以使用
Pick
执行此操作。我们还将保留一个字段
\uuu 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 }
})
总而言之:

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();
type IsValidArg=T扩展对象?凯奥夫特永远不会延伸?假:真:真;
类型函数到构造函数=
T扩展(a:推断a,b:推断b)=>void?
是真的吗?新建(p1:A,p2:B)=>T返回:

IsValidArg

非常确定,使用通用的清洁剂可以使其非常干燥。类似于
函数类(名称:string,定义:T):ActualClass
的东西。棘手的部分是构造函数的参数。你可能需要重复一下。至于扩展,我认为它太有活力了,太老套了。打字脚本和元编程不能很好地结合在一起。我认为我们可以得到像样的打字,比任何一种都好,或者重新定义所有的类型。我不确定今晚我是否能做到,但如果没有其他人做到,我会在早上回答:-)天哪,这是一种完全不同的编程形式(就像编程类型一样)。谢谢你的洞察力。还没有问题。必须去学习这种元类型。@trusktr是的,这是另一种编程方式:)有趣的事实,类型脚本类型系统正在图灵完成自己嗨,提香,我一直在学习我的其他问题(感谢其他答案!),现在我开始更好地理解这一切。实际问题要难一点。下面是一个关于
Class()
API在普通JS中的完整图片:。如您所见,在子对象中定义了静态、私有和受保护的成员。有没有可能将那些私有/受保护/静态成员输入为这样的类型?我得说,这太令人印象深刻了!与常规类相比,这种方法的一个缺点是,当鼠标悬停在变量上时(例如,
bigDog
),不容易理解类型工具提示。对于常规类,我们会在工具提示中看到
const bigDog:Dog
,但在游乐场示例中,工具提示显示:
const bigDog:Pick&Pick
。我想这是没有办法的?@trusktr我想我们甚至可以在工具提示方面改进。。静态和私密乍一看是可行的(我刚刚醒来,所以请恕我直言:P)。在gitter上与我联系,很难在评论中进行对话,这可能需要很多来回才能正确进行。。
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();