使用TypeScript在接口中定义静态方法详解
作者:it键盘侠
静态方法
静态方法或静态属性是存在于类的任何实例中的属性,它们是在构造函数级别定义的,也就是说,类本身具有这些方法,因此这些类的所有实例也将具有这些方法。
例如,当我们创建一个域对象或数据库实体时,就会用到常见的静态方法:
class Person { static fromObject (obj: Record<string, unknown>) { const instance = new Person() instance.prop = obj.prop return instance } toObject () { return { prop: this.prop } }
fromObject
方法存在于所有类中,它位于任何实例之上,因此不能使用 this 关键字,因为 this 尚未初始化,而且你所处的上下文高于 this 可以引用的任何实例。
在本例中,我们接收了一个对象,并直接用它创建了一个新的类实例。要执行这段代码,请不要执行类似以下的标准操作
const p = new Person() p.fromObject(etc) // error, the property does not exist in the instance
我们需要直接从类的构造函数中调用该方法:
const p = Person.fromObject(etc)
引出的问题
静态方法在强类型语言中非常常见,因为类的静态时刻和 "动态 "时刻之间有明确的区分。
但是,当我们需要使用静态类型对动态语言进行类型化时,会发生什么情况呢?
在 TypeScript 中,当我们尝试声明一个类有动态方法和静态方法,并尝试在接口中描述这两种方法时,就会出现一些错误:
interface Serializable { fromObject (obj: Record<string, unknown>): Person toObject (): Record<string, unknown> } class Person implements Serializable // Class 'Person' incorrectly implements interface 'Serializable'. // Property 'fromObject' is missing in type 'Person' but required in type // 'Serializable'.
出现这种情况的原因是,TypeScript 中的接口作用于类的 dynamic side(动态端)
,因此就好像所有接口都是相关类的实例,而不是类本身。
幸运的是,TypeScript 提供了一种将类声明为构造函数的方法,即所谓的构造函数签名(Constructor Signatures):
interface Serializable { new (...args: any[]): any fromObject(obj: Record<string, unknown>): Person toObject(): Record<string, unknown> }
现在应该能用了吧?遗憾的是,即使你手动实现了该方法,该类仍然会说你没有实现 fromObject 方法。
静态反射问题
例如,如果我们想创建一个数据库类,直接使用类中的实体名称来创建文件,这可以通过任何类中的 name 属性来实现,这是一个静态属性,存在于所有可实例化的对象中:
interface Serializable { toObject(): any } class DB { constructor(entity: Serializable) { const path = entity.name // name does not exist in the property } }
好了,我们可以将 entity.name
替换为 entity.constructor.name
,这也行得通,但当我们需要从一个对象创建一个新实体时怎么办呢?
interface Serializable { toObject(): any } class DB { #entity: Serializable constructor(entity: Serializable) { const path = entity.constructor.name this.#entity = entity } readFromFile() { // we read from this file here const object = 'file content as an object' return this.#entity.fromObject(object) // fromObject does not exist } }
因此,我们有一个选择:要么优先处理实例,要么优先处理构造函数...
解决方案
幸运的是,我们有办法解决这个问题。我们定义接口的两部分,即静态部分和实例部分:
export interface SerializableStatic { new (...args: any[]): any fromObject(data: Record<string, unknown>): InstanceType<this> } export interface Serializable { id: string toJSON(): string }
需要注意的是,in 中的构造函数的类型
new(...args: any[]): any
必须与 return 中的类型相同any
,否则就会成为循环引用
有了类的这两部分类型,我们可以说类只实现了实例部分:
class Person implements Serializable { // ... }
现在,我们可以说我们的数据库将接收两种类型的参数,一种是静态部分,我们称之为 S,另一个是动态(或实例)部分,我们称之为 I,S 将始终扩展 SerializableStatic而 I 将始终扩展 Serializable,默认情况下,它将是 S 的实例类型,可以通过 InstanceType<S>
类型使用程序来定义:
class Database<S extends SerializableStatic, I extends Serializable = InstanceType<S>> {}
例如,现在我们可以正常使用我们的属性:
class Database<S extends SerializableStatic, I extends Serializable = InstanceType<S>> { #dbPath: string #data: Map<string, I> = new Map() #entity: S constructor(entity: S) { this.#dbPath = resolve(dirname(import.meta.url), `.data/${entity.name.toLowerCase()}.json`) this.#entity = entity this.#initialize() } }
在 #initialize 方法中,我们将使用 fromObject 方法直接读取文件,并将其转化为一个类的实例:
class Database<S extends SerializableStatic, I extends Serializable = InstanceType<S>> { #dbPath: string #data: Map<string, I> = new Map() #entity: S constructor(entity: S) { this.#dbPath = resolve(dirname(import.meta.url), `.data/${entity.name.toLowerCase()}.json`) this.#entity = entity this.#initialize() } #initialize() { if (existsSync(this.#dbPath)) { const data: [string, Record<string, unknown>][] = JSON.parse(readFileSync(this.#dbPath, 'utf-8')) for (const [key, value] of data) { this.#data.set(key, this.#entity.fromObject(value)) } return } this.#updateFile() } }
此外,我们还可以使用 get 和 getAll 等方法,甚至是只接收和返回实例的保存方法。
get(id: string): I | undefined { return this.#data.get(id) } getAll(): I[] { return [...this.#data.values()] } save(entity: I): this { this.#data.set(entity.id, entity) return this.#updateFile() }
现在,当我们使用这种类型的数据库时,例如
class Person implements Serializable { // enter code here } const db = new DB(Person) const all = db.getAll() // Person[] const oneOrNone = db.get(1) // Person | undefined db.save(new Person()) // DB<Person>
以上就是使用TypeScript在接口中定义静态方法详解的详细内容,更多关于TypeScript定义静态方法的资料请关注脚本之家其它相关文章!