问题
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