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

时间:2018-06-17 18:31:08

标签: javascript typescript types

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

库允许我们通过将对象文字传入API来定义一个类,如下所示,我想知道如何使它返回一个与编写常规{{{名}有效相同的类型。 1}}:

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() API接受用于定义类的对象文字。

如何输入此API,以便最终结果是Class().extends()Animal在TypeScript中的行为就像我使用原生Dogclass Animal {}语法编写它们一样?

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

EDIT1:通过在JavaScript中编写类并在class Dog extends Animal {}类型定义文件中声明常规class {}定义,似乎可以轻松地键入我使用lowclass创建的类。如果可能的话,将我的低类代码库转换为TypeScript似乎更加困难,以便它可以在定义类时自动进行键入,而不是为每个类生成.d.ts个文件。

EDIT2:我想到的另一个想法是我可以按原样保留lowclass(JavaScript类型为.d.ts),然后当我定义类时,我可以使用any来定义它们{} 1}}可以是同一文件中的类型声明。这可能比使lowclass成为TypeScript库更简单,因此类型是自动的,因为我必须重新声明我在使用lowclass API时已经定义的方法和属性。

1 个答案:

答案 0 :(得分:1)

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

类型和值

在typescript类中有一个特殊的地方,它们代表一个值(构造函数是一个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是一个简单的函数,而不是构造函数,typescript不会让我们在一个简单函数上调用new(至少不是在严格模式下)。为了解决这个问题,我们可以使用条件类型在构造函数和原始函数之间进行映射。这种方法与here类似,但我只是为了保持简单而只编写几个参数,你可以添加更多:

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,我们可以使用{{1}执行此操作}。我们还会保留一个字段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将在对象文字中定义的函数中具有curent对象的类型。问题是我们需要在扩展现有类型时指定this的类型,因为this将包含当前对象文字的成员加上基类型的成员。幸运的是,typescript允许我们使用编译器使用的标记类型this来执行此操作并描述here

创建扩展

现在使用contextual,我们可以创建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'>>>
}

希望这会有所帮助,请告诉我是否可以提供更多帮助:)

Playground link