Recursive generic function with type passed as a parameter

旧巷老猫 提交于 2020-06-01 05:22:07

问题


I want to have a recursive generic function, but I cannot use type passed as an argument in generic function invocation, cause of

'memberType' refers to a value, but is being used as a type here.

Is there a way to pass memberType to generic method invocation?

Example:

class Packet {
  header: Header
  body: Body

  static MEMBERS = [
    ['header', Header, 0,  6],
    ['body',   Body,   6, 10],
  ]
}

class Header {
  size: number
  ttl: number

  static MEMBERS = [
      ['size', 'number',   0,  2],
      ['ttl',  'number',   2,  3],
  ]
}

class Body {
  raw: string

  static MEMBERS = [
      ['raw', 'string',   0,  10]
  ]
}




class Deserializer {
  static Deserialize<T>(serialized: string, type: { new(): T ;}): T {
      const obj = new type();

      const members = obj.constructor['MEMBERS'];

      for(const member of members) {
          var [memberName, memberType, startOffset, len] = member;
          const serializedMember = serialized.substr(startOffset, len) // cut string holding serialized member (like 'qwertyuiop' from example)

          if(memberType == 'number') { // handle primitive type differently
            obj[memberName] = parseInt(serializedMember); 
          } else if(memberType == 'string') { // handle primitive type differently
            obj[memberName] = serializedMember

          } else { // handle non primitives, like Header and Body

// *** issue is on the following line ***
            obj[memberName] = Deserialize<memberType>() // 'memberType' refers to a value, but is being used as a type here.
          }

      }

      return obj as T;
  }
}

//                  HEADRBOOOOOOODY - description of `serialized` string below
//                  SSTTL (where SS = header.size, TTL = header.ttl)  
const serialized = '11222qwertyuiop'
const packet: Packet = Deserializer.Deserialize<Packet>(serialized, Packet)

// expected `packet` object structure after running `Deserialize`
// Packet
//   * header -> Header
//                 * size -> 11
//                 * ttl  -> 222
//   * body -> Body
//               * raw -> BOOOOOOODY

回答1:


Right, memberType is a value, not a type. You could get its type from the compiler as typeof memberType, but in your case the recursive call to Deserialize needs arguments, from which the generic type parameter can be inferred as typeof memberType if you pass it in:

obj[memberName] = Deserializer.Deserialize(serializedMember, memberType);

That should clear up the problem you asked about, but the example code has a lot of type errors and implicit any issues which you should probably address.


One way to address them is via slight refactoring and the judicious use of type assertions to deal with the places that the compiler can't verify as safe (e.g. obj[memberName] = parseInt(...) won't work without an assertion because the compiler won't realize that the property at memberName should be a number, since it's a higher order inference it can't perform). Maybe something like:

type DeserializableClass<T> = {
  new(): T,
  MEMBERS: readonly {
    [K in keyof T]: readonly [K, string | DeserializableClass<T[K]>, number, number]
  }[keyof T][]
}

class Deserializer {
  static Deserialize<T>(serialized: string, type: DeserializableClass<T>): T {
    const obj = new type();

    const members = type.MEMBERS;

    for (const member of members) {
      var [memberName, memberType, startOffset, len] = member;
      const serializedMember = serialized.substr(startOffset, len) // cut string holding serialized member (like 'qwertyuiop' from example)

      if (typeof memberType !== "string") {
        obj[memberName] = Deserializer.Deserialize(serializedMember, memberType);
      } else if (memberType == 'number') { // handle primitive type differently
        obj[memberName] = parseInt(serializedMember) as any as T[keyof T];
      } else if (memberType == 'string') { // handle primitive type differently
        obj[memberName] = serializedMember as any as T[keyof T];
      }

    }
    return obj;
  }
}

Here we've described more exactly what kind of class can be deserialized: it has to have a MEMBERS property with the right sort of array of tuples. Note that this isn't perfectly type safe because T[keyof T] is too wide, and you can keep changing things to be safer, but at that point I'd be inclined to make your MEMBERS property hold deserializers for primitives also and give up on the strings. Note how the compiler sees the recursive Deserialize call as acceptable, but it still needs to be told that the string and number cases work.

Also, in order for this to accept Packet you need to ensure that your MEMBERS static properties are sufficiently literal (tuples tend to get widened to arrays); an easy way to do that is with a const assertion, like:

static MEMBERS = [
  ['size', 'number', 0, 2],
  ['ttl', 'number', 2, 3],
] as const

Anyway I won't go farther into the refactoring than this for now. Hope this helps. Good luck!

Playground link to code



来源:https://stackoverflow.com/questions/62104545/recursive-generic-function-with-type-passed-as-a-parameter

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!