Редактировать: я полностью изменил вопрос после того, как сегодня немного поиграл с этим: Детская площадка
Я начинаю с конца, с примера, который для простоты хочет получить правильный первый аргумент, предоставленный пользователем:
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 с сохраненной информацией о типе, и что я получу правильный тип возврата: неопределенный или строковый, в зависимости от предоставленного значения. Однако это «любой» тип. Можно ли что-то сделать в этой ситуации или мне просто не повезло?
В конце концов, я ожидал бы, что возврат для вызова с неопределенным будет функцией с неопределенными аргументами, тогда как никакой неопределенный вызов не вернет какое-то значение, подумайте о частичном применении.
@jcalz Сегодня я попробовал немного поиграть с этим и обновил свой вопрос
Соответствует ли такой подход вашим потребностям? Обратите внимание, что часть, в которой сохраняется тип первого аргумента, почти не имеет ничего общего с частью, в которой вы разрешаете добавлять undefined
. Я бы даже не стал ставить это в вопрос; вы в основном решили ту часть, где вы разрешаете неопределенное значение типа FnWithOptions
. Может быть, вы могли бы отредактировать, чтобы полностью удалить его? Но в любом случае дайте мне знать, если мое предложение выше полностью решает вопрос. Если да, то я напишу ответ; если нет, то что мне не хватает?
Спасибо, это помогло мне заставить отношения между Params и Args работать должным образом, теперь я надеюсь завершить свою первоначальную идею. Эта часть TypeScript немного сбивает меня с толку относительно того, как общие параметры и аргументы взаимодействуют в более сложных отношениях, я думаю, что, к сожалению, в документации это не очень подробно описано. Еще раз спасибо, это было очень полезно.
Было бы здорово, если бы вы случайно могли уточнить часть «<A расширяет FnWithOptions<P>>(...args: A)» - я не знаю, как это лучше всего описать, но, например, «< A расширяет любой[]>(...args: A)" интуитивно понятен - A определил ограничение, и компилятор заменяет его простым для понимания способом, первый, однако, не интуитивно понятен, поскольку кажется, что он ссылается на самого себя, что каким-то образом работает, но я действительно не видел, чтобы это много документировалось или обсуждалось, и работаю с этим на основе попытки/неудачи/повторения.
Я объяснил это в своем ответе, как мог.
Если у вас есть кортеж типа 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]
.
Детская площадка, ссылка на код
Спасибо за хороший ответ, как всегда полезно.
Каков твой вопрос?