[英]How can we type a class factory that generates a class given an object literal?
例如,我創建了一個名為lowclass
的 JavaScript 庫,我想知道如何讓它在 TypeScript 類型系統中工作。
該庫讓我們通過將對象字面量傳入 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"
如您所見, Class()
和Class().extends()
API 接受用於定義類的對象字面量。
我如何鍵入這個 API 以便最終結果是Animal
和Dog
在 TypeScript 中的行為就像我使用本機class Animal {}
和class Dog extends Animal {}
語法編寫它們一樣?
即,如果我要將代碼庫從 JavaScript 切換到 TypeScript,在這種情況下我如何鍵入 API 以便最終結果是使用我的低級類的人可以像使用常規類一樣使用它們?
EDIT1:通過在 JavaScript 中編寫class {}
並在.d.ts
類型定義文件中聲明常規class {}
定義,這似乎是一種簡單的方法來輸入我用 lowclass 創建的class {}
。 如果可能的話,將我的低級代碼庫轉換為 TypeScript 似乎更加困難,以便在定義類時自動輸入而不是為每個類制作.d.ts
文件。
EDIT2:我想到的另一個想法是我可以保留 lowclass 原樣(JavaScript 類型為any
),然后當我定義類時,我可以使用as SomeType
定義它們as SomeType
其中SomeType
可以是同一文件中的類型聲明。 這可能比使 lowclass 成為 TypeScript 庫更簡單,因此類型是自動的,因為我必須重新聲明我在使用 lowclass API 時已經定義的方法和屬性。
好的,我們需要解決幾個問題,以使其以類似於 Typescript 類的方式工作。 在我們開始之前,我在 Typescript strict
模式下完成以下所有編碼,如果沒有它,某些鍵入行為將無法工作,如果您對解決方案感興趣,我們可以確定所需的特定選項。
在打字稿中,類占有特殊的位置,因為它們代表一個值(構造函數是一個 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
是一個簡單的函數,而不是一個構造函數,並且打字稿不會讓我們在一個簡單的函數上調用new
(至少不是在嚴格模式下)。 為了解決這個問題,我們可以使用條件類型在構造函數和原始函數之間進行映射。 該方法與此處類似,但我將只為幾個參數編寫它以保持簡單,您可以添加更多參數:
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
領域,我們將asume一個空的構造,我們必須刪除constructor
由新構造函數,我們將回到返回的類型,我們可以做到這一點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
在類型的方法中正確鍵入,這很好,並且適用於對象文字。 對於對象字面量, this
將具有對象字面量中定義的函數中當前對象的類型。 問題是我們需要在擴展現有類型時指定this
的類型,因為this
將具有當前對象字面量的成員加上基類型的成員。 幸運的是,打字稿讓我們可以使用ThisType<T>
來做到這一點,這是編譯器使用的標記類型,並在此處進行了描述
現在使用上下文 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'>>>
}
把它們放在一起:
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();
希望這會有所幫助,如果我能提供更多幫助,請告訴我:)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.