Как заставить TypeScript возвращать другое значение в зависимости от необязательного аргумента

Я пишу обертку fetch(), которая принимает аргумент параметров. Обычно эта оболочка использует верблюжий регистр возвращаемого значения, но если для параметра dontCamelize установлено значение true, тогда она должна обернуть возвращаемое значение в общий Decamelize<>. Однако я не уверен, как выразить это в TypeScript.

Поскольку свойств параметров может быть несколько, я не думаю, что смогу использовать переопределения функций. Я пытался выразить это с помощью помощника ResponseType и условного типа, но, похоже, это не сработало. Любая помощь очень ценится!

Этот код генерируется с помощью инструмента генерации кода спецификации OpenAPI, который всегда передает первый параметр типа, поэтому он всегда будет выглядеть следующим образом:

const res = fetcher<{fooBar: 'baz'}>({ fooBar: 'baz' }); // Expect: { fooBar: 'baz' }

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

type Decamelize<T> = {
  [K in keyof T as K extends string ? `${Uncapitalize<K>}` : K]: T[K]
};

type Options = {
  dontCamelize?: boolean;
  verbose?: boolean;
  // Add more options here as needed
  option1?: string;
  option2?: number;
  option3?: boolean;
};

type ResponseType<T, O extends Options> = 
  O['dontCamelize'] extends true ? Decamelize<T> : T;

export function fetcher<T, O extends Options = {}>(
  subject: T,
  options: O = {} as O
): ResponseType<T, O> {
  return subject as any;
}

// Usage examples
const res1 = fetcher<{ fooBar: 'baz' }>({ fooBar: 'baz' }); // Expect: { fooBar: 'baz' }
const res2 = fetcher<{ fooBar: 'baz' }>({ fooBar: 'baz' }, { dontCamelize: true }); // Expect: Decamelize<{ fooBar: 'baz' }>
const res3 = fetcher<{ fooBar: 'baz' }>({ fooBar: 'baz' }, { verbose: true }); // Expect: { fooBar: 'baz' }
const res4 = fetcher<{ fooBar: 'baz' }>({ fooBar: 'baz' }, { dontCamelize: true, verbose: true }); // Expect: Decamelize<{ fooBar: 'baz' }>

Является ли dontCamelize единственным параметром, влияющим на тип возвращаемого значения?

Darryl Noakes 14.08.2024 02:48

Удалите параметр явного типа, и все заработает. Когда вы указываете один, TS использует значение по умолчанию для другого. Если вы удалите их все, они выведут их все, включая объект параметров.

Darryl Noakes 14.08.2024 02:53

@Darryl Noakes - да, dontCamelize - единственный вариант, который меняет тип ответа

robdodson 14.08.2024 03:36

@DarrylNoakes Я не уверен, о каком явном параметре типа вы говорите. Вы не против опубликовать ответ?

robdodson 14.08.2024 03:39

о, я думаю, вы говорите о том, где я вызываю сборщик <{ fooBar: 'baz' }>(...) Вы правы, это работает, если я это удалю, но, к сожалению, мне нужно передать тип в позвони на сайт вот так. Фактический код, с которым я работаю, передаст этот тип на другой уровень утилите внутри сборщика. Он генерирует код из HTTP-клиента OpenAPI, и я несколько ограничен в его структуре.

robdodson 14.08.2024 03:44

Правильный. Завтра попробую написать ответ, если еще никто не ответил

Darryl Noakes 14.08.2024 03:47

Я не думаю, что в настоящее время это возможно без изменения кода вызова. См. Предложение: вывод аргумента частичного типа, которое похоже на вашу проблему. Это требует от вас явного запроса вывода дополнительных параметров. Там могут быть обходные пути.

Darryl Noakes 14.08.2024 16:02

Пожалуйста, уточните: (1) Можете ли вы изменить код сайта вызова или нет? (2) Вы пишете функцию fetcher и вызываете внутри нее сгенерированный код?

Darryl Noakes 14.08.2024 16:22

Если вы пишете это и вам просто нужно передать тип функции, вызываемой внутри, вывод должен быть в порядке. Но я не думаю, что это так?

Darryl Noakes 14.08.2024 16:24

Рабочая версия с использованием перегрузок: tsplay.dev/NBKZpm

Darryl Noakes 14.08.2024 16:28

Спасибо @DarrylNoakes, это выглядит великолепно!

robdodson 14.08.2024 18:07
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой 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 для повышения производительности приложения путем загрузки модулей только тогда, когда они...
3
11
85
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Удалите явные общие параметры <{ fooBar: 'baz' }>:

const res1 = fetcher({ fooBar: 'baz' });
const res2 = fetcher({ fooBar: 'baz' }, { dontCamelize: true });
const res3 = fetcher({ fooBar: 'baz' }, { verbose: true });
const res4 = fetcher({ fooBar: 'baz' }, { dontCamelize: true, verbose: true });

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

Как указано в комментариях после того, как вы опубликовали свой ответ и позже отредактировали вопрос, этот подход невозможен.

Darryl Noakes 14.08.2024 15:51

Вам не нужно его передавать, вы можете использовать ResponseType<T, O> внутри функции fetcher. А если оно находится за пределами, оно вам не нужно.

Remo H. Jansen 14.08.2024 16:15

Сайт вызова должен иметь вид const res2 = fetcher<{ fooBar: 'baz' }>({ fooBar: 'baz' }, { dontCamelize: true }) с включенным явным параметром типа. Этот код не находится под контролем ОП, поэтому его нельзя удалить. Если ОП правильно объяснил.

Darryl Noakes 14.08.2024 16:21

Тогда ты мало что сможешь сделать. Я бы попробовал использовать что-то вроде ts-morph.com, чтобы найти узлы вызова функции с именемfetcher и удалить параметры типа из узла вызова функции. Я бы выполнил этот шаг перед шагом сборки TSC по умолчанию.

Remo H. Jansen 14.08.2024 16:43

Да, к сожалению, необходимо передать первый параметр типа. Код генератора выдает что-то вроде: httpClient<GetBillsResponse>( { url: /bills, method: 'GET', params }, options ) Я контролирую только реализацию объекта httpClient.

robdodson 14.08.2024 17:52
Ответ принят как подходящий

Пока существует только один* параметр, влияющий на тип возвращаемого значения, вы можете использовать перегрузки.

* Ну, можно было бы и больше, но вам придется указать перегрузку для каждой комбинации. Два — это практический максимум.

(1) Измените тип Options на общий.

Первый шаг — добавить параметр универсального типа к объекту параметров, чтобы мы могли различать значения dontCamelize.

Измените это:

type Options = {
  dontCamelize?: boolean
}

на это:

type Options<dontCamelize extends boolean = boolean> = {
  dontCamelize?: dontCamelize
}

(2) Измените ResponseType, чтобы использовать параметр логического типа вместо типа «Параметры».

Измените это:

type ResponseType<T, O extends Options> = 
  O['dontCamelize'] extends true ? Decamelize<T> : T

на это:

type ResponseType<T, dontCamelize extends boolean> = 
  dontCamelize extends true ? Decamelize<T> : T

(3) Функция изменена с использования параметра универсального типа на перегрузки.

Используемая перегрузка зависит от типа dontCamelize в переданном объекте параметров, что позволяет нам установить тип возвращаемого значения на его основе.

Изменения:

  • Удалите параметр универсального типа для options.
  • Добавьте две перегрузки: одну для того, чтобы dontCamelize было ложным или не указано, и одну для того, чтобы оно было истинным.
    Обобщенному типу возвращаемого значения (ResponseType) передается соответствующий логический тип для dontCamelize.

Измените это:

export function fetcher<T, O extends Options = {}>(
  subject: T,
  options: O = {} as O
): ResponseType<T, O>

на это:

// Blank lines added for readability. They're optional.

// Overload signature: `dontCamelize` is not specified or false
export function fetcher<T>(
  subject: T,
  options?: Options<false>
): ResponseType<T, false>

// Overload signature: `dontCamelized` is true
export function fetcher<T>(
  subject: T,
  options: Options<true>
): ResponseType<T, true>

// Implementation signature
export function fetcher<T>(
  subject: T,
  options: Options = {}
): ResponseType<T, boolean>

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

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

Использование дженериков для сопоставления одного строкового литерала в Typescript
Что означает `T расширяет только чтение неизвестно [] | []` в сигнатуре функции TypeScript `Promise.all`?
Репликация агрегации проекта mongodb в Typescript
Могу ли я сделать вывод intellisense более читабельным для универсального Typescript с помощью Omit?
Как создать универсальный крючок для извлечения данных в TypeScript для API с различными структурами ответов?
Проблема вывода типа с массивом универсальных объектов конфигурации с разными внутренними типами, которые зависят от другого внутреннего свойства
Как обеспечить соблюдение типа универсального компонента Vue для нескольких свойств
Требовать, чтобы абстрактный класс Typescript определял свойства из другого интерфейса, которые должны быть определены в родительском классе
Как использовать рекурсивный кортеж с переменным числом аргументов в качестве типа параметра функции
TS2344: тип, выведенный из свойства универсального аргумента, «не удовлетворяет ограничению», хотя тип работает нормально?