在打字稿中使用扩展表达式推断通用元组

Inferring a generic tuple with spread expressions in typescript

(TS 版本 - 4.1)

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

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' } ])

*update:更新了函数以阐明我为什么明确关心 Xs 和 Ys:实际的 return 类型是一个元组,其各个元素可以是X 或 Y 取决于原始 arg

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

我已经尝试了一些东西

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] }
  ]
}

想知道是否有人对如何使推理起作用有任何想法correctly/am我是不是想错了?

谢谢!

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

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

的函数
// 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 被推断为元组类型而不仅仅是数组类型,您可以使用 variadic tuple types 来获得此行为,方法是将 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;}]

正在计算 return 类型:没有注释产生 unknown[]

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

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

如果不注解return类型,编译器会判断为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() 的 return 值推断强类型,我们需要 assert 它作为 T 的函数。但是T的功能是什么?


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

嗯,对于每个数字索引 I,您查看元素 T[I] 和 return 其 "x" 属性 类型 T[I]["x"] 或其类型 T[I]["y"]"y" 属性。因此,这里的第一步是 return 这些类型的 union

// 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]

这样更好;我们有一个长度为 4 的元组,其中大部分是 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 的类型。


计算return类型:存在x,不存在y,不知道则联合

那么我们重新定义上面的XorY<E>类型,让它对数组的元素类型E做如下分析:

  • 如果E有一个X类型的非可选x属性,我们应该returnX.
  • 如果 E 没有 x 属性 但它有 y 属性 类型 Y,我们应该 return Y.
  • 否则,如果 E 有一个 X 类型的可选 x 属性 和一个 [=29] 类型的 y 属性 =], 那么我们应该 return 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 的函数; Yconditional type inference but for X I am indexing into E directly; that's because X will not include undefined if you use infer, but optional properties are sometimes undefined (give or take the --exactOptionalProperties compiler flag 一起找到)。所以 E['x']undefined 时比 infer 更准确。

无论如何,有了 XY,我们 return X | Y 如果 x 是已知密钥 ('x' extends keyof E) 并且如果它是可选的 ({} extends Pick<E, 'x'>)。如果已知但不是可选的,我们 return X。如果不知道,那么我们 return 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]

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

Playground link to code

还有一个替代版本:

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 }])

Playground

为了更好地理解 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)])
}

如果您想了解有关函数参数推断的更多信息,可以阅读我的 article