Расширение типа объекта аргумента функции

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


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 может измениться, я ищу общее решение)

Мне очень трудно понять, что такое Expand и ExpandData. написание {[K in string]: unknown} означает, что вы заботитесь об индексных подписях, а написание {[K in ⋯ as K | string]: ⋯} означает, что вы практически уничтожаете всю информацию о K (если K не является числом или символом). Это странный рекурсивный тип, и мне неочевидно, какова его цель. О какой функции вы говорите, аргумент которой вы хотите расширить? Пожалуйста, отредактируйте это, чтобы уточнить, что вы пытаетесь сделать. Прямо сейчас я озадачен.

jcalz 28.06.2024 19:25

Я понимаю, что текущая реализация типа некорректна, я оставил ее для примера. Я отредактировал его, описав какой тип хочу получить в результате

Vitaliy Severniy 28.06.2024 20:37

Предполагая, что я понимаю (что может быть неправильно, поскольку вы показали только один тестовый пример), напрямую это невозможно. «Функция с любым именем» не является конкретным типом. Хуже того, невозможно сказать «любое имя, кроме keyof XXX» (см. ms/TS#17867 ), поэтому вы не можете сделать это произвольно, не испортив при этом существующие свойства. Возможно, вы можете проверить такое ограничение, но это означает, что вам понадобится вспомогательная функция, как показано в этой ссылке на игровую площадку, и тогда TS тоже не захочет делать выводы val.

jcalz 28.06.2024 21:35

(см. предыдущий комментарий). Это полностью решает вопрос? Если да, то я мог бы написать ответ (хотя это несколько разочаровывает). Если нет, то что мне не хватает?

jcalz 28.06.2024 21:37

Да, я пришел к варианту через вспомогательную функцию, но подумал, что это можно как-то реализовать таким образом. Большое спасибо за ответ

Vitaliy Severniy 28.06.2024 21:52

Было бы намного лучше, если бы у вас было согласованное имя свойства для функции «изменение», а не просто «любое имя», что в любом случае странно (как бы вы его узнали?). Возможно ли это?

jcalz 28.06.2024 21:54

первоначальная идея заключалась в том, чтобы иметь уникальное имя, чтобы можно было добавлять неограниченное количество функций, таких как приращение:(значение) => значение+ 1, уменьшение:(значение) => значение - 1, а затем возвращать эти функции. для реализации придется использовать дополнительную функцию, но это будет выглядеть менее удобно

Vitaliy Severniy 28.06.2024 23:44

Таким образом, вы по-прежнему можете иметь неограниченное количество функций, если воспользуетесь подходом, показанным по ссылке на эту игровую площадку. Можно ли использовать известный ключ, например fns, для объекта функций? Этот подход работает намного лучше (посмотрите, как выводится val). Если это работает для вас, я был бы рад написать ответ, показывающий это и почему это лучше. Если нет, то почему?

jcalz 29.06.2024 02:39

Я считаю этот вариант идеальным, спасибо

Vitaliy Severniy 29.06.2024 08:16
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
1
9
63
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

На самом деле не существует конкретного полезного типа, который работал бы таким образом. Вы хотели бы сказать, что разрешите 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

Теперь это работает так, как вам хотелось бы.

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

Другие вопросы по теме