TypeScript: можно ли вывести аргумент типа для каждого элемента массива отдельно?

Предположим, у нас есть следующий код:

// a value, and a function that will be called with this value
type ValueAndHandler<T> = [value: T, handler: (value: T) => void]

type Params = {
    // not sure how to type it
    valuesAndHandlers: ValueAndHandler<???>[]
    // it's not the only field in the structure
    // so I cannot just pass array as variadic arguments
    otherStuff: number
}

function doSomethingWithParams(params: Params){/* something, not important*/}

doSomethingWithParams({
    // array of some pairs of handlers and values
    // each value is only related to its handler, and not to other handlers/values
    valuesAndHandlers: [
        [5, value => console.info(value.toFixed(3))],
        ["example", value => console.info(value.length)]
    ],
    otherStuff: 42
})

В этом коде я хочу, чтобы тип параметра первого обработчика выводился как number, а тип параметра второго обработчика выводился как string; но этого не происходит (потому что я набрал их как unknown в определении переменной).

Как я могу ввести определение, чтобы правильный тип для каждого обработчика выводился индивидуально?

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

Соответствует ли этот подход с использованием сопоставленных типов кортежей и универсальной вспомогательной функции вашим потребностям? Если это так, я напишу ответ с объяснением; если нет, то что мне не хватает?

jcalz 12.04.2023 01:51

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

Nartallax 12.04.2023 02:02

Либо вам нужно сделать Params общим, как это показывает (и вывод немного страдает из-за того, что это находится внутри вложенного свойства, поэтому вам придется немного больше аннотировать), либо вам нужно провести рефакторинг, потому что на самом деле нет хороший специфический тип, соответствующий ValueAndHandler<???>; вы бы искали экзистенциально количественные дженерики, а TS не поддерживает их напрямую. Вы можете подражать им, как это показывает, но это немного странно. ...

jcalz 12.04.2023 02:17

... Каков вариант использования массива этих вещей, если вы не знаете, что такое аргументы типа? Предположительно, вы можете сделать только vh[1](vh[0]), и в этом случае вы можете просто выставить только ту функциональность, как это показывает. Какой из этих трех подходов, если таковые имеются, вы бы хотели, чтобы я написал в качестве ответа?

jcalz 12.04.2023 02:17

Да, первый подход - это как раз то, что мне нужно! Я смог его использовать. Не думал, что можно использовать такие кортежи. Вы можете написать ответ, и я приму его. Большое спасибо!

Nartallax 12.04.2023 02:22

Какая версия, эта? Если это так, то это по сути то же самое, что и самое первое, что я опубликовал, а также то же самое, что и ответ Адси ниже ... и я не уверен, что хочу приложить усилия, чтобы опубликовать свой собственный ответ. Или это одна из версий?

jcalz 12.04.2023 02:24

Ага, этот. Это не совсем то же самое; ключевое отличие в том, что я не знал, что вы можете расширить отображаемый тип в такой кортеж

Nartallax 12.04.2023 02:27

🤔 О, так вы просто говорите об использовании синтаксиса вариативного кортежа [...X] вместо синтаксиса X, чтобы намекнуть, что мы хотим, чтобы X выводился как тип кортежа, а не неупорядоченный массив? Я думаю, что этой разницы достаточно, чтобы иметь значение ... Я напишу ответ, когда у меня будет шанс.

jcalz 12.04.2023 02:29
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой 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
8
82
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Если вам нужен такой вывод, у вас нет другого выбора, кроме как запустить оболочку fn :


function valueAndHandlers<T extends any[]>(
  ...arg:{ [I in keyof T]: [T[I], (arg: T[I]) => void] }
) {
  return [arg]
}

// This will return them wrapped in an outer array. Need to use varargs to get inference I think.

valueAndHandlers(
  [5, value => console.info(value.toFixed(3))],
  ["example", value => console.info(value.length)]
) 

// As used in your edit

doSomethingWithParams({
    // array of some pairs of handlers and values
    // each value is only related to its handler, and not to other handlers/values
    valuesAndHandlers: valueAndHandlers(
        [5, value => console.info(value.toFixed(3))],
        ["example", value => console.info(value.length)]
    )
})

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

Nartallax 12.04.2023 02:15

Это довольно распространенный подход (обертка FN). Обычно для пользователей библиотеки это необязательно использовать, и только если они хотят использовать intellisense. Конечно, помимо этого вам нужен условный тип для doSomethingWithParams, чтобы убедиться, что он проверен, чтобы гарантировать, что они не могут ничего использовать, когда они не используют оболочку (покажет ошибку TS, если кортежи не совпадают, но никаких выводов).

adsy 12.04.2023 02:16
Ответ принят как подходящий

Предполагая, что вы действительно хотите отслеживать аргументы кортежа универсального типа для каждого элемента вашего массива, тогда вам нужно сделать Params универсальным в этом типе кортежа:

type Params<T extends any[]> = {
  valuesAndHandlers: [...{ [I in keyof T]: ValueAndHandler<T[I]> }]
}

Здесь свойство valuesAndHandlers представляет собой сопоставленный тип кортежа , где каждый элемент T[I] типа ввода T (по индексу I) обернут ValueAndHandler, чтобы стать ValueAndHandler<T[I]>. Я также обернул весь отображаемый тип кортежа в вариативный тип кортежа , например [...{ ⋯ }]; на самом деле это не меняет тип, но дает компилятору подсказку, что когда он выводит тип, он должен попытаться вывести кортеж вместо неупорядоченного типа массива произвольной длины (это описано в microsoft/TypeScript#). 39094: «Тип [...T], где T — параметр типа, подобный массиву, можно удобно использовать для указания предпочтения для вывода типов кортежей»).

Чтобы сократить количество аннотаций типов, которые вам нужно написать, мы можем создать вспомогательную функцию для вывода конкретного аргумента T с учетом некоторого Params<T>:

const asParams = <T extends any[]>(p: Params<T>) => p;

И мы можем использовать это так:

let params = asParams({
  valuesAndHandlers: [
    [5, (value: number) => console.info(value.toFixed(3))],
    ["example", (value: string) => console.info(value.length)]
  ]
});
/* let params: Params<[number, string]> */

Таким образом, params был выведен как Params<[number, string]>, как и хотелось.

Обратите внимание, что для того, чтобы это работало, мне пришлось аннотировать типы параметров в обратных вызовах. Тип контекстной типизации, который выводит тип value в value => console.info(value.length) из его контекста, не работает с выводом из сопоставленных кортежей внутри свойств. В зависимости от того, насколько вы заботитесь, это можно смягчить путем рефакторинга функции asParams(), чтобы она принимала значение свойства valuesAndHandlers в качестве отдельного аргумента... или, на самом деле, любого количества возможных рефакторингов для настройки вывода.

Главный концептуальный момент здесь заключается в том, что Params должен быть общим, а все остальное — это в основном просто методы, позволяющие получить желаемый вывод.

Площадка ссылка на код

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