Мутирующий тип основан на методах - PullRequest
2 голосов
/ 17 января 2020

У меня есть класс, который должен позволить вам проанализировать Buffer с цепочкой

class Parser<T> {
  private buffer: Buffer;
  private offset: number;
  private varsInternal: Record<>;

  constructor(buffer: Buffer) {
    this.offset = 0;
    this.buffer = buffer;
    this.varsInternal = {};
  }

  char(name: string): this {
    const val = this.buffer.readUInt8(this.offset);
    this.varsInternal[name] = val;
    this.offset += 1;
    return this;
  }

  short(name: string): this {
    const val = this.buffer.readUInt16BE(this.offset);
    this.varsInternal[name] = val;
    this.offset += 2;
    return this;
  }

 vars(): T {
    return this.varsInternal;
  }
}

Итак, вы можете сделать следующее:

const data = new Parser(myBuffer)
  .char("myChar")
  .char("otherChar")
  .short("myShort")
  .vars();

console.log(data); // { myChar: 32, otherChar: 123, myShort: 123544 }

Но я должен вручную напишите типы для этого:

const data = new Parser(myBuffer)
  .char("myChar")
  .char("otherChar")
  .short("myShort")
  .vars();

console.log(data.myChar);
              // ^^^^^^ does not exist

const data2 = new Parser<{ myChar: number, otherChar: number, myShort: number }>(myBuffer)
  .char("myChar")
  .char("otherChar")
  .short("myShort")
  .vars();

console.log(data.myChar); // works

Вы можете видеть, что это становится уродливым.

Поэтому мне интересно, возможно ли записать возвращаемые значения char и short в способ, который сообщает TypeScript, что vars будет добавлять поле каждый раз, когда я соединяюсь с char и short

Ответы [ 2 ]

1 голос
/ 17 января 2020

Возможная идея - расширять возвращаемый тип Parser<T> при каждом добавлении нового свойства. В этом случае T - это тип, представляющий всю структуру данных Record.

class Parser<T extends Record<string, number> = Record<string, number>> {
    private buffer: Buffer;
    private offset: number;
    private varsInternal: T

    constructor(buffer: Buffer) {
        this.offset = 0;
        this.buffer = buffer;
        this.varsInternal = {} as T;
    }

    char<K extends string>(name: K): Parser<T & Record<K, number>> {
        const val = this.buffer.readUInt8(this.offset); // returns number
        this.varsInternal[name] = val;
        this.offset += 1;
        return this as Parser<T & Record<K, number>>
    }

    // change short method analogue to char above

    vars() {
        return this.varsInternal;
    }
}

Например, вызов myParser.char("myChar") вернет Parser<T & Record<"myChar", number>> - предыдущая запись данных T расширена или пересекается с Record, содержащим новый добавленный ключ K, который здесь равен "myChar".

Некоторое тестирование доказывает, что теперь у нас есть сильный тип:

const data = new Parser({/* buffer */ })
    .char("myChar")
    .char("otherChar")
    .vars();

data.otherChar // works, number
data.myChar // works, number

Пример кода

0 голосов
/ 17 января 2020

Давайте создадим псевдоним типа AddProp<T, K, V>, который принимает тип объекта T, тип ключа K и тип значения V и создает новый тип объекта со всеми свойствами из * 1005. * а также новое свойство с ключом K и значением V. Вот один из способов его определения:

type AddProp<T, K extends PropertyKey, V> =
    (T & { [P in K]: V }) extends infer O ? { [P in keyof O]: O[P] } : never;

Тип (T & {[P in K]: V}) сам по себе сделает эту работу, но в конце вашего цепного вызова вы получите уродливый тип, такой как AddProp<AddProp<{myChar: number;}, "otherChar", number>, "myShort", number>, поэтому я Я использую трюк с условным типом , который расширяет свойства в один тип объекта, такой как {myChar: number; otherChar: number; myShort: number}.


Вооружившись этим, давайте начнем переопределять типы Parser:

class Parser<T = {}> {
    private buffer: Buffer;
    private offset: number;
    private varsInternal: T;

    constructor(buffer: Buffer) {
        this.offset = 0;
        this.buffer = buffer;
        this.varsInternal = {} as T;
    }

Здесь я установил значение по умолчанию , значение T быть пустым типом объекта {}. И мы хотим, чтобы varsInternal имел тип T, и мы должны утверждать , что начальное значение {} для varsInternal соответствует T (что не произойдет, если вы вручную укажите T как в new Parser<{a: number}>(buffer) ... так что не делайте этого).

Теперь давайте добавим закрытый метод для повторного использования, который добавит свойство с ключом типа K и значением типа V до this.varsInternal и возвращаем this, где мы утверждаем, что возвращенный this будет иметь тип Parser<AddProp<T, K, V>>. Идея состоит в том, что это позаботится о мутации типов в других ваших методах:

    private addProp<K extends PropertyKey, V>(prop: K, val: V): Parser<AddProp<T, K, V>> {
        (this.varsInternal as any)[prop] = val;
        return this as any;
    }

А вот ваши char() и short() методы. Обратите внимание, что они должны быть generi c в свойстве name, поскольку string слишком широкий, чтобы отслеживать конкретные ключи, добавленные к T.

    char<K extends PropertyKey>(name: K) {
        const val = this.buffer.readUInt8(this.offset);
        const ret = this.addProp(name, val);
        this.offset += 1;
        return ret;
    }

    short<K extends PropertyKey>(name: K) {
        const val = this.buffer.readUInt16BE(this.offset);
        const ret = this.addProp(name, val);
        this.offset += 2;
        return ret;
    }

И ваш vars() такой же:

    vars(): T {
        return this.varsInternal;
    }
}

Давайте проверим это:

declare const myBuffer: Buffer;

const data = new Parser(myBuffer)
    .char("myChar")
    .char("otherChar")
    .short("myShort")
    .vars();
/* const data: {
    myChar: number;
    otherChar: number;
    myShort: number;
}*/

console.log(data.myChar); // number

Выглядит хорошо. Восстановленный тип data равен {myChar: number, otherChar: number, myShort: number}. Если эти значения должны быть другого типа, вы должны обработать val внутри char() и / или short() перед вызовом this.addProp().

В любом случае, я остановлюсь на этом; надеюсь это поможет. Удачи!

Детская площадка ссылка на код

...