Я думаю, что наиболее разумный конкретный тип для представления ваших данных - это что-то вроде:
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
});
Хорошо, надеюсь, это поможет; удачи!
Ссылка на код