TypeScript:类中动态声明的方法

Elm*_*lmo 2 javascript types class typescript

我有一些代码,例如:

const methodsList = [
  'foo',
  'bar',
  // ... 20 other items ...
]

export class Relayer {
  constructor() {
    for (const methodName of methodsList) {
      this[methodName] = (...args) => {
        // console.log('relaying call to', methodName, args)
        // this is same for all methods
      }
    }
  }
}

const relayer = new Relayer()

relayer.foo('asd') // TS error
relayer.bar('jkl', 123) // TS error
Run Code Online (Sandbox Code Playgroud)

现在,当我使用类实例时,当我调用relayer.foo()或时,TypeScript 会发出抱怨relayer.bar()。为了使代码编译,我必须对其进行强制转换as any或类似的操作。

foo我有一个声明和bar其他方法的接口:

interface MyInterface {
  foo: (a: string) => Promise<string>
  bar: (b: string, c: number) => Promise<string>
  // ... 20 other methods
}
Run Code Online (Sandbox Code Playgroud)

如何让 TypeScript 学习动态声明的方法foobar类方法?declare语法在这里有用吗?

Art*_*yer 7

第一步是创建一个类型或接口,当按 中的值索引时methodsList,结果将是一个函数:

// The cast to const changes the type from `string[]` to
// `['foo', 'bar']` (An array of literal string types)
const methodsList = [
    'foo',
    'bar'
] as const

type HasMethods = { [k in typeof methodsList[number]]: (...args: any[]) => any }

// Or
type MethodNames = typeof methodsList[number]  // "foo" | "bar"
                   // k is either "foo" or "bar", and obj[k] is any function
type HasMethods = { [k in MethodNames]: (...args: any[]) => any }
Run Code Online (Sandbox Code Playgroud)

然后,在构造函数中,为了能够分配 的键methodsList,您可以添加一个类型断言this is HasMethods

// General purpose assert function
// If before this, value had type `U`,
// afterwards the type will be `U & T`
declare function assertIs<T>(value: unknown): asserts value is T

class Relayer {
    constructor() {
        assertIs<HasMethods>(this)
        for (const methodName of methodsList) {
            // `methodName` has type `"foo" | "bar"`, since
            // it's the value of an array with literal type,
            // so can index `this` in a type-safe way
            this[methodName] = (...args) => {
                // ...
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,在构建之后,您仍然必须转换类型:

const relayer = new Relayer() as Relayer & HasMethods

relayer.foo('asd')
relayer.bar('jkl', 123)
Run Code Online (Sandbox Code Playgroud)

您还可以在使用工厂函数构造时摆脱强制转换:

export class Relayer {
    constructor() {
        // As above
    }

    static construct(): Relayer & HasMethods {
        return new Relayer() as Relayer & HasMethods
    }
}

const relayer = Relayer.construct()
Run Code Online (Sandbox Code Playgroud)

另一种解决方法是创建一个新类和类型断言,从而new生成一个HasMethods对象:

class _Relayer {
    constructor() {
        assertIs<HasMethods>(this)
        for (const methodName of methodsList) {
            this[methodName] = (...args) => {
                // ...
            }
        }
    }
}

export const Relayer = _Relayer as _Relayer & { new (): _Relayer & HasMethods }

const relayer = new Relayer();

relayer.foo('asd')
relayer.bar('jkl', 123)
Run Code Online (Sandbox Code Playgroud)

或者,如果您只使用newand then 中的方法methodsList,您可以这样做:

export const Relayer = class Relayer {
    constructor() {
        assertIs<HasMethods>(this)
        for (const methodName of methodsList) {
            this[methodName] = (...args) => {
                // ...
            }
        }
    }
} as { new (): HasMethods };
Run Code Online (Sandbox Code Playgroud)

您还可以使用您的MyInterface界面而不是HasMethods,跳过第一步。这也为您的调用提供了类型安全。