Вопрос или проблема
У меня есть код, который выглядит так:
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 }`
Я, конечно, ожидаю, что элемент 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;
Теперь этот пример работает, но есть и другие проблемы с ним в аналогичных, но более нюансированных случаях:
- Если
GetterOrStatic
(или что-то другое, что я использую там), может принимать только строки), возникает ошибка типа наT[K]
. Playground - Если я использую массивы вместо объектов, это заставляет меня использовать условие
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 }
Объяснение решения
-
Использование Extract: В определении функции
f
, возвращаемый тип изменен наExtract<T, string>
. Это гарантирует, что еслиT
— это строка, то она будет возвращена, иначе возвращается как объект. - Итоговый вывод: Теперь для обоих случаев,
x
иy
, функцияf
правильно выводит типы как{ x: string }
и{ y: string }
соответственно.
Заключение
Этот подход должен устранить проблемы с выводом типов, сохранив гибкость функции. Если у вас возникнут дополнительные проблемы, рекомендую протестировать ваш код в TypeScript Playground и вносить изменения в зависимости от ваших реальных требований. Если у вас есть более специфические примеры, с которыми вы сталкиваетесь, поделитесь ими, и мы сможем рассмотреть их вместе.