例如,我创建了一个名为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中的行为就像我使用原生Dog
和class 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时已经定义的方法和属性。
答案 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'>>>
}
希望这会有所帮助,请告诉我是否可以提供更多帮助:)