例如,我已经制作了一个名为lowclass
的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中的行为就像我使用本机类Animal {}和Dog扩展Animal {}语法编写的一样?
即,如果我要将代码库从JavaScript切换到TypeScript,在这种情况下我应该如何键入API,这样最终结果是使用我的由低类制作的类的人可以像常规类一样使用它们?
EDIT1:通过用JavaScript编写低类创建的类,并在.d.ts类型定义文件中声明常规类{}定义,这似乎是键入我使用低类创建的类的简便方法.如果可能的话,将我的低级代码库转换为TypeScript似乎更加困难,以便在定义类时可以自动进行键入,而不是为每个类都创建.d.ts文件.
EDIT2:我想到的另一个想法是,我可以保留低级类(JavaScript输入为任何类型),然后在定义类时,我可以使用SomeType定义它们,其中SomeType可以是同一文件内的类型声明.这可能比使lowclass成为TypeScript库要少一些,因此类型是自动的,因为我必须重新声明使用lowclass API时已经定义的方法和属性.
解决方法:
好的,因此我们需要解决一些问题,才能使其以与Typescript类类似的方式工作.在开始之前,我将在下面的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
构造函数
第二个问题是构造函数是一个简单函数,而不是构造函数和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函数,该函数将接收对象文字并构建一个类似于声明的类的类型.如果这里没有构造函数字段,我们将假定一个空的构造函数,并且必须从返回的新构造函数返回的类型中删除该构造函数,可以使用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声明中,可以在类型的方法中正确键入它,这很好,并且非常适合对象文字.对于对象文字,在对象文字中定义的函数中将具有当前对象的类型.问题在于,在扩展现有类型时,我们需要指定此类型,因为它将包含当前对象文字的成员以及基本类型的成员.幸运的是,打字稿允许我们使用ThisType< T>编译器使用并描述了here的标记类型
创建扩展
现在使用上下文此功能,我们可以创建扩展功能,要解决的唯一问题是,我们需要查看派生类是否具有它自己的构造函数,或者我们可以使用基本构造函数,用新类型替换实例类型.
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();
希望这会有所帮助,让我知道我是否还能提供其他帮助:)