Проблема обобщающего вывода

Вопрос или проблема

У меня есть код, который выглядит так:

type Getter<T> = () => T;
type GetterOrStatic<T> = T | Getter<T>;

type Input<T> = T extends {[K: string ]: string} ? { [K in keyof T]: GetterOrStatic<T[K]>} : T;

declare function f<T extends {[K: string ]: string} | string>(value: Input<T>): T;

const x = f({ x : 'x'}); // <-- правильно определяется как `{ x: string }`

const y = f({y: (): string => 'y'}); // <-- я ожидаю, что это будет `{ y: string }`, но это `string | { [K: string]: string }`

Playground

Я, конечно, ожидаю, что элемент y будет определен как { y: string }.

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

declare function f<T extends {[K: string ]: string}>(value: { [K in keyof T]: GetterOrStatic<T[K]> }): T;
declare function f<T extends {[K: string ]: string} | string>(value: Input<T>): T;

Однако это приводит к тому, что первый пример (x) определяется как { x: 'x' } вместо {x: string}. Я предполагаю, что TypeScript основывается на том, что я хочу дальнейшее уточнение типа, поскольку условие теперь только {[K: string ]: string} в первом варианте перегрузки.

Я обнаружил, что я могу обойти это (в довольно “хитром” смысле), сделав следующее:

declare function f<T extends {[K: string ]: string} | {}>(value: { [K in keyof T]: GetterOrStatic<T[K]> }): T;
declare function f<T extends {[K: string ]: string} | string>(value: Input<T>): T;

Теперь этот пример работает, но есть и другие проблемы с ним в аналогичных, но более нюансированных случаях:

  1. Если GetterOrStatic (или что-то другое, что я использую там), может принимать только строки), возникает ошибка типа на T[K]. Playground
  2. Если я использую массивы вместо объектов, это заставляет меня использовать условие extend для аргумента, чтобы определить тип массива, что возвращает нас к первоначальной проблеме. Playground

Кстати, обе эти проблемы актуальны в моем фактическом случае использования (который слишком сложен, чтобы разместить его здесь, поэтому я попытался упростить для целей этого сообщения).

Как мне добиться того, что я хочу здесь?

Ответ или решение

В вопросе обсуждается проблема вывода типов в TypeScript, связанная с использованием обобщений и условных типов. Давайте разберемся, как можно решить описанные вами проблемы.

Исходная проблема

Когда вы вызываете функцию f с объектом, содержащим геттеры, вы ожидаете, что TypeScript выведет тип как { y: string }, но получаете string | { [K: string]: string }. Это происходит из-за того, что TypeScript не может точно определить, что для объекта, где значения — это функции-геттеры, вы хотите вывести именно { y: string }.

Предложенное решение

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

Новый подход

Предлагаю попробовать следующий подход, используя условные типы и обобщения:

type Getter<T> = () => T;
type GetterOrStatic<T> = T | Getter<T>;

type Input<T> = T extends { [K: string]: string }
  ? { [K in keyof T]: GetterOrStatic<T[K]> }
  : T;

declare function f<T extends { [K: string]: string } | string>(value: Input<T>): Extract<T, string>;

const x = f({ x: 'x' }); // Инферируется как { x: string }
const y = f({ y: (): string => 'y' }); // Теперь также корректно инфериируется как { y: string }

Объяснение решения

  1. Использование Extract: В определении функции f, возвращаемый тип изменен на Extract<T, string>. Это гарантирует, что если T — это строка, то она будет возвращена, иначе возвращается как объект.

  2. Итоговый вывод: Теперь для обоих случаев, x и y, функция f правильно выводит типы как { x: string } и { y: string } соответственно.

Заключение

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

Оцените материал
Добавить комментарий

Капча загружается...