无法使TypeScript泛型链工作

3
我正在构建一个函数,接受一个函数链,其中每个函数都会转换前一个函数的结果。在给定的示例中,_context 应该等于 { hello: 'world', something: 'else' }
function withWorld<Context extends {}>(context: Context) {
  return { ...context, hello: 'world' }
}

function withSomething<Context extends {}>(context: Context) {
  return { ...context, something: 'else' }
}

test([withWorld, withSomething], (_context) => {})

然而,当我试图使用泛型来描述这个行为时,我遇到了一个错误:

No overload matches this call:
  Types of parameters 'contextSomething' and 'contextB' are incompatible.
    Type 'unknown' is not assignable to type '{}'.

这是代码 (TypeScript playground):
export interface TestFunction {
  <A>(
    conditions: [(contextSolo: {}) => A],
    body: (contextBodySolo: A) => any
  ): any

  <A, B>(
    conditions: [(contextA: {}) => A, (contextB: A) => B],
    body: (contextBody: B) => void
  ): any
}

const test: TestFunction = () => void 0

function withWorld<Context extends {}>(contextWithWorld: Context) {
  return { ...context, hello: 'world' }
}

function withSomething<Context extends {}>(contextSomething: Context) {
  return { ...context, something: 'else' }
}

test([withSomething], (_context) => {})
test([withWorld], (_context) => {})
test([withWorld, withSomething], (_context) => {})

当数组中只有一个函数时,TypeScript可以正确推断类型,即使示例更复杂并具有初始状态。(完整错误信息请参见TypeScript playground)。

No overload matches this call.
  Overload 1 of 2, '(conditions: [(contextSolo: {}) => any], body: (contextBodySolo: any) => any): any', gave the following error.
    Argument of type '[<Context extends {}>(contextWithWorld: Context) => any, <Context extends {}>(contextSomething: Context) => any]' is not assignable to parameter of type '[(contextSolo: {}) => any]'.
      Source has 2 element(s) but target allows only 1.
  Overload 2 of 2, '(conditions: [(contextA: {}) => unknown, (contextB: unknown) => any], body: (contextBody: any) => void): any', gave the following error.
    Type '<Context extends {}>(contextSomething: Context) => any' is not assignable to type '(contextB: unknown) => any'.
      Types of parameters 'contextSomething' and 'contextB' are incompatible.
        Type 'unknown' is not assignable to type '{}'.(2769)
2个回答

3

以下是一个可用的示例:

export interface TestFunction<Ctx> {
  <A>(
    conditions: [(context: Ctx) => A],
    body: (context: A) => any
  ): any

  <A, B>(
    conditions: [(context: Ctx) => A & Ctx, (context: A & Ctx) => B],
    body: (context: B & Ctx) => void
  ): any
}

const test: TestFunction<{ hello: 'world' }> = () => void 0

function withWorld<Context extends { hello: 'world' }>(context: Context) {
  return { ...context, world: 'hello' }
}

function withSomething<Context extends {}>(context: Context) {
  return { ...context, something: 'else' }
}

test([withSomething], (_context) => void 0)
test([withWorld], (_context) => void 0)
test([withWorld, withSomething], (_context) => void 0)

我在每个参数中明确添加了 Ctx 作为交集。

更新

这里提供一个通用解决方案:

type Fn = (...args: any[]) => any
type MapObject<T extends Fn> = ReturnType<T>
type Elem = Fn

type Mapper<
  Arr extends ReadonlyArray<Elem>,
  Result extends Record<string, any> = {}
  > = Arr extends []
  ? Result
  : Arr extends [infer H]
  ? H extends Elem
  ? Result & MapObject<H>
  : never
  : Arr extends readonly [infer H, ...infer Tail]
  ? Tail extends ReadonlyArray<Elem>
  ? H extends Elem
  ? Mapper<Tail, Result & MapObject<H>>
  : never
  : never
  : never


type Foo = { foo: 'foo' }
type Bar = { bar: 'bar' }
type Baz = { baz: 'baz' }

type Result = Mapper<[(arg: number) => Foo, (arg: Foo) => Bar, (arg: Bar) => Baz]> // Foo & Bar & Baz

export interface TestFunction<Ctx> {
  <A>(
    conditions: [(context: Ctx) => A],
    body: (context: A) => any
  ): any

  <A, B, C extends ReadonlyArray<any>>(
    conditions: C,
    body: (context: Mapper<C, Ctx>) => void
  ): any
}

const test: TestFunction<{ hello: 'world' }> = () => void 0

function withWorld<Context extends { hello: 'world' }>(context: Context) {
  return { ...context, world: 'hello' }
}

function withSomething<Context extends {}>(context: Context) {
  return { ...context, something: 'else' }
}

function withSomethingElse<Context extends {}>(context: Context) {
  return { ...context, somethingElse: 'smth else' }
}

test([withSomething], (_context) => void 0)
test([withWorld], (_context) => void 0)
test([withWorld, withSomething, withSomethingElse] as const, (_context) => void 0)

这里是其他可能会对您有所帮助的示例。

可变和不可变数组

const mutable1 = [1, 2] // number[]

const mutable2 = [{ age: 1 }, { name: 'John' }]

type MutableLength = (typeof mutable2)['length'] // number, we don't know the length

// const mutable2: ({
//     age: number;
//     name?: undefined;
// } | {
//     name: string;
//     age?: undefined;
// })[]

// As you see, if you want to operate on mutable array, TS will just make a union type from all array customElements

const immutable =  [{ age: 1 }, { name: 'John' }] as const

type ImmutableLength = (typeof immutable)['length'] // length is 2, we know exactly the length of array and the type of all elements

// Here, TS is able to say that your array has exact two elements

更新,希望是最后一次 :D

我的错,我认为用可变数组无法实现,但我应该从不同的角度看问题。

这是可行的解决方案:

type Fn = (...args: any[]) => any

// credits goes https://dev59.com/zlUL5IYBdhLWcg3wbne4#50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never;

export interface TestFunction<Ctx> {
  <A>(
    conditions: [(context: Ctx) => A],
    body: (context: A) => any
  ): any

  <C extends Array<Fn>>(
    conditions: C,
    body: (context: UnionToIntersection<ReturnType<C[number]>>) => void
  ): any
}

const test: TestFunction<{ hello: 'world' }> = () => void 0


function withWorld<Context extends { hello: 'world' }>(context: Context) {
  return { ...context, world: 'hello' }
}

function withSomething<Context extends {}>(context: Context) {
  return { ...context, something: 'else' }
}

function withSomethingElse<Context extends {}>(context: Context) {
  return { ...context, somethingElse: 'smth else' }
}

test([withSomething], (_context) => void 0)
test([withWorld], (_context) => void 0)
test([withWorld, withSomething, withSomethingElse], (_context) => void 0)

1
我正在研究通用解决方案,这是我自问的第一个问题 ;) - captain-yossarian from Ukraine
感谢您! - Sasha Koss
请记住,TS 是关于静态类型而不是动态类型的。 - captain-yossarian from Ukraine
@SashaKoss,我已经进行了最后的更新,希望这次对你有用。 - captain-yossarian from Ukraine
让我们在聊天中继续这个讨论 - captain-yossarian from Ukraine
显示剩余4条评论

0

元组是一个带有每个索引的单独类型的数组。因此,长度是固定的。

数组默认情况下具有一个类型,对于所有元素都相同,并且长度不固定。

元组必须手动强制转换。这很难看出来,因为你的例子很复杂。这样做可以消除错误 在Typescript Playground中

type  Tuple<A,B>=[(contextA: {}) => A, (contextB: A) => B]
export interface TestFunction {
  <A>(
    conditions: [(contextSolo: {}) => A],
    body: (contextBodySolo: A) => any
  ): any

  <A, B>(
    conditions: Tuple<A,B>,
    body: (contextBody: B) => void
  ): any
}

const test: TestFunction = () => void 0

function withWorld<Context extends {}>(context: Context) {
  return { ...context, hello: 'world' }
}

function withSomething<Context extends {}>(context: Context) {
  return { ...context, something: 'else' }
}

test([withSomething], (_context) => {})
test([withWorld], (_context) => {})
test([withWorld, withSomething] as Tuple<typeof withWorld, typeof withSomething>, (_context) => {})

手动强制转换在最后

[withWorld, withSomething]

被更改为

[withWorld, withSomething] as Tuple<typeof withWorld, typeof withSomething>

在TS中,手动强制转换元组是很常见的,因为默认情况下会创建一个包含所有元素逻辑OR的数组。

将其写成类型:

type Default=(withWorld|withSomething)[]

将此输入到Typescript Playground中

let a=[1,'1']

将光标放在“a”上方,它就会显示出来。

let a:(string|number)[]

而不是

let a:[string,number]

遗憾的是,该解决方案无法正确推断“_context”。此外,需要显式强制转换会产生不愉快的API。 - Sasha Koss
真的,它不知道如何在不明确传递的情况下传递部分结果。有时候简单直接是最好的选择(也许总是如此)。 - Craig Hicks

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接