在打字稿中推断具有展开表达式的通用元组



(TS 版本 - 4.1)

我正在尝试键入一个可以接受任何长度的数组/元组的泛型函数;每个元素将具有相同的属性名称,但可能的类型不同。

type Param<X = unknown, Y = unknown> = {
x?: X
y: Y
}
// return type is a fixed tuple: for each element, returns x if defined, otherwise y
function func(arg: Param[]) {
return arg.map(elem => 'x' in elem ? elem.x : elem.y )
} 

例:

func([ {x: 'string', y: 123} ])
func([ {x: 'string', y: 123}, {x: 123, y: 'string' } ])

*更新:更新了函数以明确说明为什么我关心 X 和 Y:实际返回类型是一个元组,其各个元素可以是 X 或 Y,具体取决于原始参数

我真正想做的是让编译器推断每个参数的长度和元素类型,以便相应地键入结果。

我尝试了几件事

  • 创建几个帮助程序以从Param<X,Y>中提取嵌套类型
type InferX<T extends [...any[]]> = {
[K in keyof T] : T[K] extends Param<infer X, any> ? X : never
}
type InferY<T extends [...any[]]> = {
[K in keyof T] : T[K] extends Param<any, infer Y> ? Y : never
}
为函数
  • 提供一些类型参数以包含嵌套类型,并将函数参数表示为映射到这些泛型类型的展开表达式
// this works when I provide explicit type parameters, but won't infer them implicitly from the arguments.. assumes everything is `unknown` i.e. func<Param<unknown, unknown>[], unknown[], unknown[]>
function func<
T extends Param[],
X extends { [K in keyof T]: X[K] } = InferX<T>,
Y extends { [K in keyof T]: Y[K] } = InferY<T>,
>(
arg: [...{ [K in keyof T]: Param<X[K], Y[K]> }]
) {
return arg.map(elem => 'x' in elem ? elem.x : elem.y ) as [
...{ [K in keyof T]: T[K] extends {x: X[K]} ? X[K] : Y[K] }
]
}
// application 1: works ok
func<[{x: string, y: number}, {x: number, y: string}]>([{x: 'string', y: 123}, {x: 123, y: 'string'}])
// application 2: doesn't work - types are unknown
func([{x: 'string', y: 123}])
// when I try to get more explicit with type T it won't compile at all- `A rest element type must be an array type.` error on the spread arg types
function func<
T extends { [K in keyof T]: Param<X[K], Y[K]> } & any[],
X extends { [K in keyof T]: X[K] } = InferX<T>,
Y extends { [K in keyof T]: Y[K] } = InferY<T>,
>(
arg: [...{ [K in keyof T]: Param<X[K], Y[K]> }] // ERROR
) {
return arg.map(elem => 'x' in elem ? elem.x : elem.y ) as [
...{ [K in keyof T]: T[K] extends {x: X[K]} ? X[K] : Y[K] }
]
}

想知道是否有人有任何想法如何使推理正常工作/我是否以错误的方式思考这个问题?

谢谢!

为了获得最佳结果,请使编译器的类型参数推断尽可能简单

调用泛型函数而不手动指定其类型参数时,编译器需要推断这些类型参数,通常从作为参数传递给函数的值推断。假设我们有一个函数,其调用签名为

// type F<T> = ...
declare function foo<T>(x: F<T>): void;

我们这样称呼它:

// declare const someValue: ...
foo(someValue);

然后编译器的工作是根据someValue类型和F<T>的定义来确定T。 编译器必须尝试反转F,并从F<T>推导出T。 越容易,推断的类型参数就越有可能符合您的期望。

在您的情况下,您有类似的东西

function func<
T extends Param[],
X extends { [K in keyof T]: X[K] } = InferX<T>,
Y extends { [K in keyof T]: Y[K] } = InferY<T>,
>(
arg: [...{ [K in keyof T]: Param<X[K], Y[K]> }]
): void;

但是编译器无法从类型为[...{ [K in keyof T]: Param<X[K], Y[K]> }]的值推断出TXY。 编译器要做的类型操作太多了。 如果你做一些更简单的事情,你会有更好的运气,比如

function func<T extends Param[]>(arg: T): void;

函数参数类型中的可变元组类型为编译器提供了推断元组而不仅仅是数组的提示

这有效,但编译器倾向于推断数组类型而不是元组类型:

declare function func<T extends Param[]>(arg: T): void;
func([{ x: "", y: 1 }, { x: 1, y: "" }]) 
// T inferred as Array<{x: string, y: number} | {x: number, y: string}>

由于您希望将T推断为元组类型而不仅仅是数组类型,因此可以使用可变参数元组类型来获取此行为,方法是将arg: T更改为arg: [...T]arg: readonly [...T]

declare function func<T extends Param[]>(arg: [...T]): void;
func([{ x: "", y: 1 }, { x: 1, y: "" }]) 
// T inferred as [{ x: string; y: number;}, { x: number; y: string;}]

计算返回类型:没有注释产生unknown[]

现在有一个如何处理输出类型的问题。 这不是void. 让我们检查示例代码的实现:

function func<T extends Param[]>(arg: [...T]) {
return arg.map(elem => 'x' in elem ? elem.x : elem.y)
} 

如果我们不注释返回类型,编译器将确定它是否unknown[];数组的map()方法不保留元组长度,它当然不能遵循高阶逻辑,这将为不同的索引生成不同的类型。(有关详细信息,请参阅将元组类型值映射到不带强制转换的不同元组类型值。

所以你会得到unknown[]

const p: Param<string, number> = (
[{ x: "", y: 1 }, { y: 1 }, { x: undefined, y: 1 }]
)[Math.floor(Math.random() * 3)]
const result = func([
{ x: "", y: 1 },
{ y: 1 },
{ x: undefined, y: 1 },
p
])
// const result: unknown[]

这是真的,但对你没用。 由于编译器无法推断出map()的返回值的强类型,我们需要将其断言为T函数。 但是T有什么作用呢?


计算返回类型:每个元素的xy属性的并集

好吧,对于每个数字索引I,您查看元素T[I]并返回其类型为T[I]["x"]"x"属性或类型T[I]["y"]"y"属性。 因此,第一次传递是返回这些类型的联合:

// here E is some element of the T array
type XorY<E> = E extends Param ? E["x"] | E["y"] : never;
function func<T extends Param[]>(arg: [...T]) {
return arg.map(elem => 'x' in elem ? elem.x : elem.y) as
{ [I in keyof T]: XorY<T[I]> }
}
const result = func([
{ x: "", y: 1 },
{ y: 1 },
{ x: undefined, y: 1 },
p
])
// const result: [string | number, unknown, number | undefined, string | number | undefined]

这样更好;我们有一个长度为四的元组,stringnumber类型大部分都在那里。 但是有一些缺点。

一个缺点是编译器没有意识到如果x存在,你肯定会得到它;大概对于result的第一个元素,类型应该是string,而不是string | number

另一个是,如果不知道x存在,那么unknown类型就会出来。 这在技术上实际上是正确的;编译器无法知道它为{y: 1}推断的类型意味着x属性肯定丢失。 类型{y: number}并不意味着"没有属性,但y存在";它只是意味着"没有已知的属性,但y存在"。 (也就是说,对象类型在 microsoft/TypeScript#12936 中请求的意义上不是"精确"。所以编译器决定T[I]['x']unknown,这破坏了事情。 让我们做一个技术上不正确但方便的事情,并说像{y: 1}这样的类型意味着缺少x,因此我们只想要y的类型出来。


计算返回类型:x是否存在,如果不存在,则y并集(如果不知道)

因此,让我们从上面重新定义XorY<E>类型,以便它对数组的元素类型E进行以下分析:

  • 如果E有一个非可选的x属性类型X,我们应该返回X
  • 如果E没有x属性,但它具有类型Yy属性,我们应该返回Y
  • 否则,如果E有一个X类型的可选x属性和一个Y类型的y属性,那么我们应该返回X | Y

这应该涵盖所有情况(但当然可能存在边缘情况,因此您需要进行测试)。

准确检查x属性是否可选有点困难;有关详细信息,请参阅此答案。 这是编写XorY的一种方法:

type XorY<E> = E extends Param<any, infer Y> ? E['x'] extends infer X ?
'x' extends keyof E ? {} extends Pick<E, 'x'> ? X | Y : X : Y
: never : never

它以一种奇怪的方式将XY计算为E的函数;Y是用条件类型推断找到的,但X我直接索引到E;这是因为如果您使用inferX,则不会包含undefined,但有时undefined可选属性(给予或采用--exactOptionalProperties编译器标志)。因此,E['x']undefined方面比infer更准确。

无论如何,有了XY,我们返回X | Y如果x是已知的键('x' extends keyof E),如果它是可选的({} extends Pick<E, 'x'>)。 如果已知但不是可选的,我们返回X. 如果不知道,那么我们返回Y.

好吧,让我们尝试一下:

const result = func([
{ x: "", y: 1 },
{ y: 1 },
{ x: undefined, y: 1 },
p
])
// const result: [string, number, undefined, string | number | undefined]
console.log(result) // ["", 1, undefined, something]

完善!这和我们希望的一样具体。 同样,可能存在边缘情况,您应该进行测试以确保它适用于您的实际用例。 但我认为这与我所能想象的示例代码的解决方案一样接近。

操场链接到代码

还有一个替代版本:

type Elem = Param;
type HandleFalsy<T extends Param> = (
T['x'] extends undefined
? T['y']
: (
unknown extends T['x']
? T['y'] : T['x']
)
)
type ArrayMap<
Arr extends ReadonlyArray<Elem>,
Result extends any[] = []
> = Arr extends []
? Result
: Arr extends readonly [infer H, ...infer Tail]
? Tail extends ReadonlyArray<Elem>
? H extends Elem
? ArrayMap<Tail, [...Result, HandleFalsy<H>]>
: never
: never
: never;
type Param<X = unknown, Y = unknown> = {
x?: X
y: Y
}
type Json =
| null
| string
| number
| boolean
| Array<JSON>
| {
[prop: string]: Json
}

const tuple = <
X extends Json,
Y extends Json,
Tuple extends Param<X, Y>,
Arr extends Tuple[]
>(data: [...Arr]) =>
data.map(elem => 'x' in elem ? elem.x : elem.y) as ArrayMap<Arr>
// [42, "only y"] 
const result1 = tuple([{ x: 42, y: 'str' }, { y: 'only y' }])
// [42, "only y", "100"]
const result2 = tuple([{ x: 42, y: 'str' }, { y: 'only y' }, { x: '100', y: 999999 }])

操场

为了更好地理解ArrayMapHandleFalsy的内容,请查看js表示:

const HandleFalsy = (arg: Param) => {
if (!arg.x) {
return arg.y
}
return arg.x
}
const ArrayMap = (arr: ReadonlyArray<Elem>, result: any[] = []): Array<Param[keyof Param]> => {
if (arr.length === 0) {
return result
}
const [head, ...tail] = arr;
return ArrayMap(tail, [...result, HandleFalsy(head)])
}

如果你想了解更多关于函数参数推理的信息,你可以阅读我的文章

最新更新