Я хочу расширить тип аргумента функции, добавив функцию с любым именем к каждому объекту и вернув ее, но у меня возникла проблема с присвоением имени ключу добавленной функции.
export type Expand<T> = {
[K in keyof T as K]: T[K] extends { [K in string]: unknown }
? ExpandData<T[K]>
: T[K] | ((p: T) => T);
};
type ExpandData<T extends { [K in string]: unknown }> = T | Expand<T>;
export const create = <T,>(params: Expand<T>): Expand<T> => {
return params;
};
Я хочу использовать его так, где функция изменения: (val) => val вернет мне значение объекта, в котором он находится (с любым именем вместо «изменения»)
type ICreate = {
param: {
first: {
value: number;
}
}
}
const result = create<ICreate>({param: {
first: {
value: 1,
change: (val)=> val, // type val = { value: number }
}
}})
и получить в результате тип в форме
{
param: {
first: {
value: number
change: Function
}
}
}
Как это сделать правильно? (Тип ICreate может измениться, я ищу общее решение)
Я понимаю, что текущая реализация типа некорректна, я оставил ее для примера. Я отредактировал его, описав какой тип хочу получить в результате
Предполагая, что я понимаю (что может быть неправильно, поскольку вы показали только один тестовый пример), напрямую это невозможно. «Функция с любым именем» не является конкретным типом. Хуже того, невозможно сказать «любое имя, кроме keyof XXX
» (см. ms/TS#17867 ), поэтому вы не можете сделать это произвольно, не испортив при этом существующие свойства. Возможно, вы можете проверить такое ограничение, но это означает, что вам понадобится вспомогательная функция, как показано в этой ссылке на игровую площадку, и тогда TS тоже не захочет делать выводы val
.
(см. предыдущий комментарий). Это полностью решает вопрос? Если да, то я мог бы написать ответ (хотя это несколько разочаровывает). Если нет, то что мне не хватает?
Да, я пришел к варианту через вспомогательную функцию, но подумал, что это можно как-то реализовать таким образом. Большое спасибо за ответ
Было бы намного лучше, если бы у вас было согласованное имя свойства для функции «изменение», а не просто «любое имя», что в любом случае странно (как бы вы его узнали?). Возможно ли это?
первоначальная идея заключалась в том, чтобы иметь уникальное имя, чтобы можно было добавлять неограниченное количество функций, таких как приращение:(значение) => значение+ 1, уменьшение:(значение) => значение - 1, а затем возвращать эти функции. для реализации придется использовать дополнительную функцию, но это будет выглядеть менее удобно
Таким образом, вы по-прежнему можете иметь неограниченное количество функций, если воспользуетесь подходом, показанным по ссылке на эту игровую площадку. Можно ли использовать известный ключ, например fns
, для объекта функций? Этот подход работает намного лучше (посмотрите, как выводится val
). Если это работает для вас, я был бы рад написать ответ, показывающий это и почему это лучше. Если нет, то почему?
Я считаю этот вариант идеальным, спасибо
На самом деле не существует конкретного полезного типа, который работал бы таким образом. Вы хотели бы сказать, что разрешите T
иметь любой дополнительный ключ, отсутствующий в T
, и свойство этого ключа будет иметь тип (val: T) => T
. Камнем преткновения является то, что нельзя говорить «любая клавиша, кроме keyof T
». Для этого потребуется что-то вроде «подписи индекса отдыха» или «типа свойства по умолчанию», как это требуется в microsoft/TypeScript#17867 . Но это не часть языка. Различные обходные пути обсуждаются в разделе Как определить тип Typescript как словарь строк, но с одним числовым свойством «id» . Конечно, вы могли бы описать общее ограничение , которое проверяет такие объекты, но оно не позволит вам определить тип параметра добавляемых вами функций. Делать это таким образом - это беспорядок.
Было бы гораздо лучше добавить одно свойство с известным ключом (например, fns
), которое содержит значения типа (val: T) => T
... сместив эти функции на один уровень вниз. Это можно описать как определенный тип следующим образом:
type Expand<T> =
T extends object ? {
[K in keyof T]: Expand<T[K]> } &
{ fns?: { [k: string]: (val: T) => T } } :
T
Это рекурсивный условный тип , который пересекает каждый тип объекта с объектом, дополнительно содержащим свойство fns
с сигнатурой индекса. Для
type ICreate = {
param: {
first: {
value: number;
}
}
}
расширенная версия эквивалентна
type ExpandICreate = Expand<ICreate>;
/* type ExpandICreate = {
param: {
first: {
value: number;
fns?: { [k: string]: (val: { value: number }) => { value: number }}
}
fns?: { [k: string]:
(val: { first: { value: number }}) => { first: { value: number }}
}
}
fns?: { [k: string]: (val: ICreate) => ICreate }
} */
Этого достаточно, чтобы ваша исходная версия работала, но подпись индекса не «помнит», какие ключи присутствуют, а дополнительные свойства не «помнит», есть они или нет:
const create = <T,>(params: Expand<T>) => params;
const result = create<ICreate>({
param: {
first: {
value: 1,
fns: {
change: (val) => val,
}
}
}
});
result.param.first.value.toFixed(); // okay
result.param.first.fns.change({ value: 1 }); // error, fns might be undefined
result.param.first.fns?.change({ value: 1 }); // okay
result.param.first.fns?.whaaaaa({ value: 1 }); // also okay
Если вы хотите, чтобы TS знал, что fns
существует под first
и что change
существует под fns
и что whaaaaaa
не существует под fns
, то вам нужно сделать create()
еще более общим. Вы хотите написать
const create = <T, U extends Expand<T>>(params: U) => params;
но невозможно вызвать это, если вы вручную указываете T
и компилятор делает вывод U
. Это будет microsoft/TypeScript#26242 и это не часть языка. Обходной путь здесь — каррирование:
const create = <T,>() => <U extends Expand<T>>(params: U) => params;
Итак, вы вызываете create<ICreate>()
и возвращает функцию, которую вы вызываете с помощью params
:
const result = create<ICreate>()({
param: {
first: {
value: 1,
fns: {
change: (val) => val,
}
}
}
})
result.param.first.value.toFixed(); // okay
result.param.first.fns.change({ value: 1 }); // okay
result.param.first.fns.whaaaaa({ value: 1 }); // error
Теперь это работает так, как вам хотелось бы.
Мне очень трудно понять, что такое
Expand
иExpandData
. написание{[K in string]: unknown}
означает, что вы заботитесь об индексных подписях, а написание{[K in ⋯ as K | string]: ⋯}
означает, что вы практически уничтожаете всю информацию оK
(еслиK
не является числом или символом). Это странный рекурсивный тип, и мне неочевидно, какова его цель. О какой функции вы говорите, аргумент которой вы хотите расширить? Пожалуйста, отредактируйте это, чтобы уточнить, что вы пытаетесь сделать. Прямо сейчас я озадачен.