2

I have a library written in js that helps pack and unpack Buffers. It uses the builder pattern:

const bufferFormat = buffer() .int8('id') .int16('subchannel') .int32('channel') .int32('connectionId') .varString('message'); const buff = Buffer.from(...); const obj = bufferFormat.unpack(buff); /* typeof obj === { id: number; subchannel: number; channel: number; connectionId: number; message: string; } */ const buffOut = bufferFormat.pack({ id: 1, subchannel: 2, connectionId: 3, message: 'message', }); 

I want to add types to this, and ideally have it implicitly figure out the object types. The way I'm thinking of doing this is change the formatter from being builder style to taking in an array of formats in the constructor:

const bufferFormat = new BufferFormat([ { type: 'int8', name: 'id', }, { type: 'int16', name: 'subchannel', }, { type: 'string', name: 'message', }, ]); 

And then implicitly determining the pack/unpack object type to be

interface { id: number; subchannel: number, message: string, } 

This would require me to be able to infer an object type from an array of objects. Is this possible to do with typescript? Or will I have to come up with another way to add types to this buffer formatter?

    2 Answers 2

    4

    You don't have to change the implementation, it's possible to give types to the original builder object so that the compiler will be able to infer the result of unpack. We collect the names and binary types in the generic parameter of BufferBuilder, and unpack uses that parameter to convert it into object type using PropTypeMap:

    interface PropTypeMap { int8: number; int16: number; int32: number; varString: string; } type PropType = keyof PropTypeMap; interface BufferBuilder<PropTypes extends { [p in string]: PropType }> { int8<N extends string>(n: N): BufferBuilder<PropTypes & { [n in N]: 'int8' }>; int16<N extends string>(n: N): BufferBuilder<PropTypes & { [n in N]: 'int16' }>; int32<N extends string>(n: N): BufferBuilder<PropTypes & { [n in N]: 'int32' }>; varString<N extends string>(n: N): BufferBuilder<PropTypes & { [n in N]: 'varString' }>; unpack(buffer: Buffer): { [p in keyof PropTypes]: PropTypeMap[PropTypes[p]] }; pack(obj: { [p in keyof PropTypes]: PropTypeMap[PropTypes[p]] }): Buffer; } declare function buffer(): BufferBuilder<{}>; // typed implementation left as exercise const bufferFormat = buffer() .int8('id') .int16('subchannel') .int32('channel') .int32('connectionId') .varString('message'); const obj = bufferFormat.unpack({} as Buffer); // inferred as const obj: { id: number; subchannel: number; channel: number; connectionId: number; message: string; } 
    1
    • This is really cool, I didn't realize typescript supported this "recursive" kind of type inference. Maybe I should change the title of my question to better reflect this as an answer.CommentedJul 2, 2018 at 5:53
    3

    I'm assuming you don't need help with implementation; you're just trying to get the types to work out. How about something like this?

    First define the mapping from names like "int16" to their TypeScript counterpart types:

    type TypeMapping = { int8: number, int16: number, string: string, // ... add everything you need here } 

    Then describe the elements of the array you will pass into the BufferFormat constructor:

    type FormatArrayElement<K extends keyof any> = { name: K, type: keyof TypeMapping }; 

    Then, define the conversion from a FormatArrayElement to the desired interface:

    type TypeFromFormatArrayElement<F extends FormatArrayElement<keyof any>> = {[K in F['name']]: TypeMapping[Extract<F, {name: K}>['type']]} 

    That might be a bit hard to digest. Basically you're using conditional types to walk through each name property, and produce a value of the type specified by TypeMapping.

    Now, finally, we can declare the typings for the BufferFormat class:

    declare class BufferFormat<K extends keyof any, F extends FormatArrayElement<K>> { constructor(map: F[]); unpack(buffer: Buffer): TypeFromFormatArrayElement<F>; pack(obj: TypeFromFormatArrayElement<F>): Buffer } 

    In the above, note that K doesn't seem to serve any purpose. Well, it forces the compiler to narrow K to literal types, so that when you pass in something like {name: "foo", type: "int8"} to the constructor, the name property is inferred as type "foo" and not the nearly-useless string. Let's see if it works:

    declare const buffer: Buffer; const bufferFormat = new BufferFormat([ { type: 'int8', name: 'id', }, { type: 'int16', name: 'subchannel', }, { type: 'string', name: 'message', }, ]); const b = bufferFormat.unpack(buffer); b.id // number b.message // string b.subchannel // number b.oops // error 

    That works! If you try to inspect the type of b directly it will be shown as the unhelpful type alias:

    const b: TypeFromFormatArrayElement<{ type: "int8"; name: "id"; } | { type: "int16"; name: "subchannel"; } | { type: "string"; name: "message"; }> 

    but if you inspect the actual properties, it all works out as you expect.

    Hope that helps; good luck!

      Start asking to get answers

      Find the answer to your question by asking.

      Ask question

      Explore related questions

      See similar questions with these tags.