简体   繁体   English

我们如何键入一个类工厂来生成给定对象文字的类?

[英]How can we type a class factory that generates a class given an object literal?

For example, I've made a JavaScript library called lowclass and I'm wondering how to make it work in the TypeScript type system.例如,我创建了一个名为lowclass的 JavaScript 库,我想知道如何让它在 TypeScript 类型系统中工作。

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 {} :该库让我们通过将对象字面量传入 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"

As you can see, the Class() and Class().extends() API accept object literals used for defining classes.如您所见, Class()Class().extends() API 接受用于定义类的对象字面量。

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?我如何键入这个 API 以便最终结果是AnimalDog在 TypeScript 中的行为就像我使用本机class Animal {}class Dog extends Animal {}语法编写它们一样?

Ie, 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?即,如果我要将代码库从 JavaScript 切换到 TypeScript,在这种情况下我如何键入 API 以便最终结果是使用我的低级类的人可以像使用常规类一样使用它们?

EDIT1: 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. EDIT1:通过在 JavaScript 中编写class {}并在.d.ts类型定义文件中声明常规class {}定义,这似乎是一种简单的方法来输入我用 lowclass 创建的class {} 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.如果可能的话,将我的低级代码库转换为 TypeScript 似乎更加困难,以便在定义类时自动输入而不是为每个类制作.d.ts文件。

EDIT2: 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. EDIT2:我想到的另一个想法是我可以保留 lowclass 原样(JavaScript 类型为any ),然后当我定义类时,我可以使用as SomeType定义它们as SomeType其中SomeType可以是同一文件中的类型声明。 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.这可能比使 lowclass 成为 TypeScript 库更简单,因此类型是自动的,因为我必须重新声明我在使用 lowclass API 时已经定义的方法和属性。

Ok so there are several problems we need to fix for this to work in a similar way to Typescript classes.好的,我们需要解决几个问题,以使其以类似于 Typescript 类的方式工作。 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.在我们开始之前,我在 Typescript strict模式下完成以下所有编码,如果没有它,某些键入行为将无法工作,如果您对解决方案感兴趣,我们可以确定所需的特定选项。

Type and value类型和值

In typescript classes hold a special place in that they represent both a value (the constructor function is a Javascript value) and a type.在打字稿中,类占有特殊的位置,因为它们代表一个值(构造函数是一个 Javascript 值)和一个类型。 The const you define only represents the value (the constructor).您定义的const仅表示值(构造函数)。 To have the type for Dog for example we need to explicitly define the instance type of Dog to have it usable later:例如,要获得Dog的类型,我们需要明确定义Dog的实例类型以使其稍后可用:

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

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).第二个问题是constructor是一个简单的函数,而不是一个构造函数,并且打字稿不会让我们在一个简单的函数上调用new (至少不是在严格模式下)。 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;

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.使用上面的类型,我们现在可以创建简单的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'>> .如果这里没有constructor领域,我们将asume一个空的构造,我们必须删除constructor由新构造函数,我们将回到返回的类型,我们可以做到这一点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:我们还将保留一个字段__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 }
})

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.在上面的Animal声明中, this在类型的方法中正确键入,这很好,并且适用于对象文字。 For object literals this will have the type of the curent object in functions defined in the object literal.对于对象字面量, this将具有对象字面量中定义的函数中当前对象的类型。 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.问题是我们需要在扩展现有类型时指定this的类型,因为this将具有当前对象字面量的成员加上基类型的成员。 Fortunately typescript lets us do this using ThisType<T> a marker type used by the compiler and described here幸运的是,打字稿让我们可以使用ThisType<T>来做到这一点,这是编译器使用的标记类型,并在此处进行了描述

Creating 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.现在使用上下文 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'>>>
}

Putting it all together:把它们放在一起:

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 :)希望这会有所帮助,如果我能提供更多帮助,请告诉我:)

Playground link 游乐场链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM