ELLIOTTCABLE
ELLIOTTCABLE

Reputation: 18108

How do I generically iterate over the properties of an arbitrary object in TypeScript?

This is a pretty common JavaScript pattern:

function mapThruWrapper(module) {
   const replacement = {}

   Object.getOwnPropertyNames(module).forEach(function(key) {
      const val = module[key]

      if (val instanceof Function) {
         replacement[key] = wrapperFunc.bind(null, val)
      } else {
         replacement[key] = val
      }
   })

   return replacement
}

I'm trying to strongly type this in TypeScript, and I've gotten as far as something like the following:

function mapThruWrapper<M extends { [X: string]: unknown }>(module: M): M {
   const replacement: M = {}

   Object.getOwnPropertyNames(module).forEach(function(key) {
      const val = module[key]

      if (val instanceof Function) {
         replacement[key] = wrapperFunc.bind(null, val)
      } else {
         replacement[key] = val
      }
   })

   return replacement
}

Unfortunately, that's still producing errors like:

src/excmd.ts:186:10 - error TS2322: Type '{}' is not assignable to type 'M'.
  '{}' is assignable to the constraint of type 'M', but 'M' could be instantiated with a different subtype of constraint '{ [X: string]: unknown; }'.

186    const replacement: M = {}
             ~~~~~~~~~~~

src/excmd.ts:192:10 - error TS2536: Type 'string' cannot be used to index type 'M'.

192          replacement[key] = buckleScriptErrorTrampoline.bind(null, $val)
             ~~~~~~~~~~~~~~~~

How can I strongly type generic iteration over, and wrapping, of the members of an object like this?

Upvotes: 1

Views: 639

Answers (1)

user4704976
user4704976

Reputation:

I made some adjustments to the original code and added comments to explain:

function mapThruWrapper<M extends { [X: string]: unknown }>(module: M): M {
    // Add "as M" so that the compiler allows us to assign an
    // empty object (which is okay since we're populating all the
    // object's properties before the function returns anyway).
    const replacement: M = {} as M

    // Use "for in" so that the compiler is able to infer
    // that the variable "key" isn't just a string, but is
    // actually a key in module's type.
    for (const key in module) {
        if (module.hasOwnProperty(key)) {
            const val = module[key]

            if (val instanceof Function) {
                // Use "as typeof val" to let the compiler know that the
                // bound function has the same signature as the original
                // function. I'm assuming that's the case here.
                replacement[key] = wrapperFunc.bind(null, val) as typeof val
            } else {
                replacement[key] = module[key]
            }
        }
    }

    return replacement
}

Upvotes: 0

Related Questions