如何在给定对象文字的情况下键入生成类的类工厂? [英] How can we type a class factory that generates a class given an object literal?

查看:66
本文介绍了如何在给定对象文字的情况下键入生成类的类工厂?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

例如,我创建了一个名为 lowclass 我想知道如何使其在TypeScript类型系统中运行。

For example, I've made a JavaScript library called lowclass and I'm wondering how to make it work in the TypeScript type system.

该库允许我们通过传入一个对象来定义一个类 - 文字到API中,如下所示,我想知道如何让它返回一个类似于编写常规 class {} 的类型:

The library lets us define a class by passing in an object-literal into the API, like the following, and I'm wondering how to make it return a type that is effectively the same as writing a regular 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接受对象用于定义类的文字。

As you can see, the Class() and Class().extends() API accept object literals used for defining classes.

如何输入此API,以便最终结果为 Animal Dog 在TypeScript中表现得好像我用原生类动物{} 类Dog编写它们一样扩展Animal {} 语法?

How can I type this API so that the end result is that Animal and Dog behave in TypeScript as if I had written them using native class Animal {} and class Dog extends Animal {} syntax?

即如果我要将代码库从JavaScript切换到TypeScript,我该怎么输入API在这种情况下,最终的结果是使用我的使用lowclass创建的类的人可以像常规类一样使用它们吗?

I.e., if I were to switch the code base from JavaScript to TypeScript, how might I type the API in this case so that the end result is that people using my classes made with lowclass can use them like regular classes?

EDIT1:这似乎是一种简单的方法来输入我制作的类通过在JavaScript中编写它们并声明常规 class {} 定义来使用lowclass在 .d.ts 内部的类型定义文件。如果可能的话,将我的低类代码库转换为TypeScript似乎更加困难,以便它可以在定义类时自动进行输入,而不是为了生成 .d.ts 文件每个班级。

It seems an easy way to type classes I make with lowclass by writing them in JavaScript and declaring regular class {} definitions inside of .d.ts type definitions files. It seems more difficult, if even possible, to convert my lowclass code base to TypeScript so that it can make the typing automatic when defining classes rather than make .d.ts files for every class.

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

Another idea that comes to mind is that I can leave lowclass as is (JavaScript typed as any), then when I define classes, I can just define them using as SomeType where SomeType can be a type declaration right inside the same file. This might be less DRY than making lowclass be a TypeScript library so that types are automatic, as I'd have to re-declare methods and properties that I've already defined while using the lowclass API.

推荐答案

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

Ok so there are several problems we need to fix for this to work in a similar way to Typescript classes. Before we begin, I do all of the coding below in Typescript strict mode, some typing behavior will not work without it, we can identify the specific options needed if you are interested in the solution.

在打字稿类中有一个特殊的位置,因为它们代表一个值(构造函数是一个Javascript值)和一个类型。您定义的 const 仅表示值(构造函数)。要获得 Dog 的类型,我们需要显式定义 Dog 的实例类型,以便以后使用它:

In typescript classes hold a special place in that they represent both a value (the constructor function is a Javascript value) and a type. The const you define only represents the value (the constructor). To have the type for Dog for example we need to explicitly define the instance type of Dog to have it usable later:

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



构造函数的函数



第二个问题是构造函数是一个简单的函数,而不是构造函数和打字稿不会让我们在一个简单的函数上调用 new (至少不是在严格模式下)。为了解决这个问题,我们可以使用条件类型在构造函数和原始函数之间进行映射。该方法类似于此处,但我要去只为几个参数编写它以保持简单,你可以添加更多:

Function to constructor

The second problem is that constructor is a simple function, not a constructor function and typescript will not let us call a new on a simple function (at least not in strict mode). To fix this we can use a conditional type to map between the constructor and the original function. The approach is similar to here but I'm going to write it for just a few parameters to keep things simple, you can add more:

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 函数,它将接受对象文字并构建一个看起来像声明的类的类型。如果这里没有构造函数字段,我们将假设一个空构造函数,我们必须从返回的类型中删除构造函数通过我们将返回的新构造函数,我们可以使用 Pick< T,Exclude< keyof T,'constructor'>>> 来完成。我们还会保留一个字段 __ original ,以获得稍后有用的对象文字的原始类型:

Building the type

With the type above we can now create the simple Class function that will take in the object literal and build a type that looks like the declared class. If here is no constructor field, we will asume an empty constructor, and we must remove the constructor from the type returned by the new constructor function we will return, we can do this with Pick<T, Exclude<keyof T, 'constructor'>>. We will also keep a field __original to have the original type of the object literal which will be useful later:

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 声明,这个在类型的方法中正确输入,这很好,对于对象文字很有用。对于对象文字,此将具有在对象文字中定义的函数中的curent对象的类型。问题是我们需要在扩展现有类型时指定这个的类型,因为这个将有成员当前对象文字加上基类型的成员。幸运的是,typescript允许我们使用 ThisType< T> 编译器使用的标记类型并描述这里

This type in methods

In the Animal declaration above, this is typed correctly in the methods of the type, this is good and works great for object literals. For object literals this will have the type of the curent object in functions defined in the object literal. The problem is that we need to specify the type of this when extending an existing type, as this will have the members of the current object literal plus the members of the base type. Fortunately typescript lets us do this using ThisType<T> a marker type used by the compiler and described here

现在使用在上下文中,我们可以创建 extends 功能,唯一要解决的问题是我们需要查看派生类是否有自己的构造函数,或者我们可以使用基础构造函数,替换具有新类型的实例类型。

Now using contextual this, we can create the extends functionality, the only problem to solve is we need to see if the derived class has it's own constructor or we can use the base constructor, replacing the instance type with the new type.

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();

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

Hope this helps, let me know if I can help with anything more :)

游乐场链接

这篇关于如何在给定对象文字的情况下键入生成类的类工厂?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆