Типы TypeScript — доступ к аргументам функции Тип кортежа из вызова функции (тип Fn, сгенерированный другим типом)

Редактировать: я полностью изменил вопрос после того, как сегодня немного поиграл с этим: Детская площадка

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

const functionWithoutRestrictedParams = <Args extends any[]>(...args: Args): Args[0] => {
    return '' as any
}

const stringResult = functionWithoutRestrictedParams('first', 'second')

const undefinedResult = functionWithoutRestrictedParams(undefined, 'second')

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

Теперь приступим к вводу параметров функции: Сначала простой тип, который принимает типы параметров функции и делает каждый аргумент необязательным:

export type FnWithOptions<A extends any[]> = {[I in keyof A]: A[I] | undefined}

type BasicFn<Params extends any[]>
    = (...args: FnWithOptions<Params>) => void


const fn:BasicFn<[first: string, second: number]> = (first, second) => {}

fn('test', 1)
fn(undefined, 1)
//@ts-expect-error
fn(1,1)

Это сделано для того, чтобы показать, какие типы используются API. Каждый параметр может быть необязательным.

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

type MockTransformer<Params, Args> = Params

type Attempt<Params extends any[]>
    = <Args extends any[]>(...args:MockTransformer<FnWithOptions<Params>, Args>) => Args[0]

const attempt:Attempt<[first: string, second: number]> = (first, second) => {}

const expectedString = attempt("str", 1)
const expectedUndefined = attempt(undefined, 1)

Здесь я попытался объединить два предыдущих примера, сначала определив необходимые параметры, которые всегда предоставляются при определении функции. Затем определение типа функции. Я рассуждал здесь о типе MockTransformer, который принимает Params и Args, возвращает Params, так что это тип данных, который может предоставить пользователь. Затем я надеялся, что фактические аргументы, с которыми вызывается функция, будут привязаны к Args с сохраненной информацией о типе, и что я получу правильный тип возврата: неопределенный или строковый, в зависимости от предоставленного значения. Однако это «любой» тип. Можно ли что-то сделать в этой ситуации или мне просто не повезло?


50
1

Ответ:

Решено

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

<A extends P>(...args: A) => void

Теперь, если вы вызовете функцию, TypeScript выведет A на основе типов фактических аргументов. Типы этих аргументов ограничены до P, поэтому, если A равно [string], то вы не можете вызвать функцию с number.

Если функция должна возвращать тип этого первого аргумента, вы можете вернуть индексированный тип доступа A[0]:

<A extends P>(...args: A) => A[0]

В вашем конкретном случае вы на самом деле хотите принять не P, а модифицированную версию P, где каждый элемент может быть undefined. Таким образом, вместо A extends P вам нужно A extends UndefinableElements<P>, где UndefinableProps — это отображаемый массив/тип кортежа, определенный как:

type UndefinableElements<T extends any[]> =
    { [I in keyof T]: T[I] | undefined }

Итак, если P — это [string], то UndefinableElements<P> — это [string | undefined]. Итак, ваш тип функции выглядит так:

type Attempt<P extends any[]> =
    <A extends UndefinableElements<P>>(...args: A) => A[0]

Опять же, здесь происходит то, что Attempt<P> превратит тип кортежа P в версию, в которой все элементы также могут принимать undefined, а затем он станет универсальной функцией, в которой список аргументов представляет собой некоторый общий тип A, ограниченный модифицированной версией P, и где он возвращает тип первого аргумента A[0]. Давайте проверим это:

const attempt: Attempt<[first: string, second: number]> =
    (first, second) => first

const expectedString = attempt("str", 1);
//    ^? const expectedString: "str"

const expectedUndefined = attempt(undefined, 1);
//    ^? const expectedUndefined: undefined

attempt(0, 1); // error!
//      ~
// Argument of type 'number' is not assignable to parameter of type 'string'.

Выглядит неплохо. Первый вызов A выведен как ["str", 1] и, таким образом, A[0] есть "str", тогда как второй вызов A выведен как [undefined, 1] и, следовательно, A[0] есть undefined. А третий вызов выдает ошибку, поскольку тип [0, 1], который TypeScript хочет определить для A, не соответствует UndefinableElements<[first: string, second: number]>, то есть [first: string | undefined, second: number | undefined].

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


Интересные вопросы для изучения

Ошибка машинописного текста TS2307: не удается найти модуль «реагировать» при попытке импорта без использования node.js?Невозможно загрузить файл по нажатию кнопки в реакции с MuiМожно ли объединить глобальную область видимости в TypeScript?Почему [1 | 2] не является подтипом [1 | []] | [2 | []] в TypeScript?Можно ли предварительно обработать дискриминатор в дискриминируемом объединении zod?Может ли интерфейс Go получать какую-либо функцию независимо от его сигнатуры?Обработка динамических выходных данных в функциях Swift без ущерба для безопасности типов (любая)Использование протокола как типа должно быть записано с любыми (целями обучения)Какой смысл имеет оператор стрелки в Haskell в функциях, принимающих более одного параметра?Как исправить столбец с числовыми значениями, который воспринимается как строковое поле из-за пустых строк в фрейме данных Pandas?