根据Typescript中的对象数组创建对象类型

时间:2018-07-01 23:20:43

标签: typescript

我有一个用js编写的库,可以帮助打包和解压缩Buffer。它使用构建器模式:

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',
});

我想为此添加类型,理想情况下,让它隐式地找出对象类型。我正在考虑的方法是将格式化程序从生成器样式更改为在构造函数中采用一系列格式:

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

然后隐式确定打包/解压缩对象的类型

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

这将要求我能够从对象数组中推断出对象类型。这可能与打字稿有关吗?还是我必须想出另一种方法来向该缓冲区格式化程序添加类型?

2 个答案:

答案 0 :(得分:3)

您不必更改实现,可以为原始构建器对象指定类型,以便编译器能够推断unpack的结果。我们在BufferBuilder的通用参数中收集名称和二进制类型,unpack使用该参数通过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 :(得分:3)

我假设您不需要实施方面的帮助;您只是在尝试确定类型。这样的事情怎么样?

首先定义从诸如"int16"之类的名称到其TypeScript对应类型的映射:

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

然后描述您将传递给BufferFormat构造函数的数组的元素:

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

然后,定义从FormatArrayElement到所需接口的转换:

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

这可能有点难以理解。基本上,您使用conditional types遍历每个name属性,并产生TypeMapping指定的类型的值。

现在,最后,我们可以声明BufferFormat类的类型了:

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

在上面,请注意K似乎没有任何作用。好吧,它强制编译器使用narrow K to literal types,以便在将类似{name: "foo", type: "int8"}的内容传递给构造函数时,name属性被推断为类型"foo",而不是-无用的string。让我们看看它是否有效:

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

那行得通!如果您尝试直接检查b类型,它将显示为无用的类型别名:

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

但是,如果您检查实际属性,一切都会按预期进行。

希望有所帮助;祝你好运!