Определение рекурсивного интерфейса в сочетании с обобщениями - PullRequest
0 голосов
/ 01 июля 2019

Я только что столкнулся с ситуацией, в которой у меня есть некоторые неприятные вложенные данные, которые я хотел бы описать с помощью машинописных интерфейсов.

Давайте сначала посмотрим на образец данных

const d = {
    callbacks: { 
        x: { cb: (data: number) => {} },
        y: { cb: (data: string) => {} }
    },
    foo: {
        callbacks: { 
            z: { cb: (data: boolean) => {} }
        }, 
        bar: { /* .... */ }
    },
    baz: { /* .... */ }
}

Примечание: каждый уровень имеет клавишу callback (которая требуется) и несколько случайных клавиш (например, foo и bar). Также обратите внимание, что аргумент для функций может быть любым!

Результаты моей попытки создать интерфейсы для этой структуры данных:

interface DataItem<T> {
   cb: (data: T) => void;
}

interface Data {
   [key: string]: DataItem | Data;
}

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

enter image description here

Теперь я вижу 2 проблемы:

1) Интерфейс Data ничего не говорит о необходимом ключе callback 2) Интерфейс Data использует DataItem, для которого требуется аргумент

Буду признателен за любые указания о том, куда идти отсюда

1 Ответ

2 голосов
/ 01 июля 2019

Я думаю, что наиболее разумный конкретный тип для представления ваших данных - это что-то вроде:

interface Callbacks {
  [k: string]: { cb: (d: any) => void };
}

interface Data {
  callbacks: Callbacks;
  [k: string]: Callbacks | Data;
}

Основные отличия здесь от того, что вы сделали:

  • Callbacks не строго вводит параметр для функции / метода cb своих подвойств. Для разрешения чего-либо используется any.
  • Data имеет обязательное свойство callbacks типа Callbacks, в то время как другие свойства могут быть Callbacks | Data. Я знаю, что вы, вероятно, предпочли бы, чтобы другие свойства были просто Data, но, к сожалению, когда вы используете сигнатуру строкового индекса , вам необходимо убедиться, что все свойства со строковыми ключами соответствуют ему, включая определенные "callbacks" собственность. Есть способы представить более жесткие ограничения, но они, как правило, являются общими, а не конкретными типами.

В любом случае это определение примет ваши данные:

const d: Data = {
  callbacks: {
    x: { cb: (data: number) => {} },
    y: { cb: (data: string) => {} }
  },
  foo: {
    callbacks: {
      z: { cb: (data: boolean) => {} }
    },
    bar: {
      /* .... */
    }
  }
};

но, как я уже сказал, он также примет некоторые данные, которые вы можете запретить:

const oops: Data = {
  callbacks: {},
  foo: { throwbacks: { cb: (x: number) => {} } } // hmm
};

Видите, throwbacks не является callbacks, но оно принимается, потому что каждое свойство Data может принимать значение Callbacks. Это может быть не очень важно для вас. Я склонен оставить это как есть на данный момент, потому что запретить это будет означать превращение Data в универсальный тип, который вы должны указывать везде.

Другим недостатком здесь является то, что тип Data имеет несколько сигнатур индекса и any, которые заставляют его забыть о конкретном предполагаемом типе литерала вашего объекта, когда вы пытаетесь использовать его:

d.callbacks.x.cb(1); // okay
d.foo; // okay
d.callbacks.x.cb("1"); // oops, no error?
d.flop; // oops, no error?
d.foo.callbacks.z.cb(true); // oops, error?

Что бы я там предложил, если вы хотите сохранить знание своего литерала объекта, но требует, чтобы он соответствовал Data, это использовать универсальную вспомогательную функцию, которая принимает все, что соответствует Data, и возвращает свой ввод без расширения это:

const dataHelper = <D extends Data>(d: D) => d;

И используйте это так:

const d2 = dataHelper({
  callbacks: {
    x: { cb: (data: number) => {} },
    y: { cb: (data: string) => {} }
  },
  foo: {
    callbacks: {
      z: { cb: (data: boolean) => {} }
    },
    bar: {
      /* .... */
    }
  }
});

d2.callbacks.x.cb(1); // okay
d2.foo; // okay
d2.callbacks.x.cb("1"); // error as desired
d2.flop; // error as desired
d2.foo.callbacks.z.cb(true); // okay as desired

Мы могли бы на этом остановиться, но если вы действительно хотите сузиться до конкретного типа Data и если вы не возражаете против сложности, мы можем заставить помощника принудительно заставить тип его аргументов строго соответствовать "имеет callbacks свойство типа Callbacks и все остальные свойства типа Data ":

type DataConstraint<T extends Data> = {
  [K in keyof T]: K extends "callbacks"
    ? Callbacks
    : T[K] extends Data ? DataConstraint<T[K]> : Data
};

const dataHelper2 = <D extends Data & DataConstraint<D>>(d: D) => d;

То, что DataConstraint - это сопоставленный и условный тип , который представляет ограничение, что только свойство "callbacks" должно иметь тип Callbacks. Давайте посмотрим, как это действует:

const d3 = dataHelper2({
  callbacks: {
    x: { cb: (data: number) => {} },
    y: { cb: (data: string) => {} }
  },
  foo: {
    callbacks: {
      z: { cb: (data: boolean) => {} }
    },
    bar: { // error! missing callbacks ?
      /* ... */
    }
  }
});

Эй, это дало ошибку, которую я пропустил ... свойство bar в foo отсутствует необходимое callbacks. И мы также запрещаем плохое значение oops до:

const oops2 = dataHelper2({
  callbacks: {},
  foo: { throwbacks: { cb: (x: number) => {} } } // error! not Data
});

Хорошо, надеюсь, это поможет; удачи!

Ссылка на код

...