简体   繁体   English

在 typescript 中链接 function 类型

[英]Chaining function types in typescript

If one has an array of functions, where the output from one is the input into the next function ie inputs and outputs types must be aligned for each pair, but may differ across pairs.如果一个函数具有一组函数,其中一个函数的 output 是下一个 function 的输入,即输入和输出类型必须为每对对齐,但在不同对之间可能不同。 How can one enable to Typescript compiler to understand the typing:如何启用 Typescript 编译器来理解打字:

type A = () => string
type B = (str: string)=> number
type C = (num: number)=> [number,number]
const a:A = ()=>'1'
const b:B = (str)=>parseInt(str)
const c:C = (num)=>[num,num]

//typescript is fine with this
console.log(`result: ${c(b(a()))}`)

//but not what follows, even though it's similar functionality
//this is not dynamic typing as the order in which functions
//are dealt with is known and fixed at compile time

const arrayOfFunctions: [A,B,C] = [a,b,c]

let prevResult: any
arrayOfFunctions.forEach(fn=>{
  // nasty hack that breaks typing by casting everything to any
  const hackedFn : (res?:any)=>any = fn as (res?:any)=>any
  if(prevResult) prevResult = hackedFn(prevResult)
  else prevResult = hackedFn()
})
console.log(`prevResult: ${prevResult}`)

How can one execute arrayOfFunctions without breaking the typing?如何在不中断输入的情况下执行arrayOfFunctions

code 代码

Here is some code to build a processor with tuple types you provided and do something with type checking.这是一些代码,用于使用您提供的元组类型构建处理器并进行类型检查。

type A = () => string
type B = (str: string) => number
type C = (num: number) => [number, number]
const a: A = () => '1'
const b: B = (str) => parseInt(str)
const c: C = (num) => [num, num]

const arrayOfFunctions: [A, B, C] = [a, b, c]

function builder<T extends Array<any>>(array: T) {
  return function (step: (opt: {
    [Index in keyof T]: {
      index: Index,
      value: T[Index]
      next: (opt: T[Index]) => void
    }
  }[number]) => void) {
    let prevValue: T[number] | undefined = undefined
    for (let ind = 0; ind < array.length; ind++) {
      if (ind === 0) {
        prevValue = array[ind]()
      }
      else {
        prevValue = array[ind](prevValue)
      }
      let nextValue: T[number] | undefined = undefined
      let nextCalled = false
      step({
        index: ind,
        value: prevValue,
        next: function (v) {
          nextValue = v
          nextCalled = true
        }
      })
      if (nextCalled === false) {
        throw 'next() not called.'
      }
      prevValue = nextValue
    }
    return prevValue
  }
}

let ret = builder(arrayOfFunctions)(function (opt) {
  if (opt.index == '0') { // Don't use ===
    console.log(opt.index, opt.value)
    opt.next(opt.value)
  }
  if (opt.index == '1') { // Don't use ===
    console.log(opt.index, opt.value)
    opt.next(opt.value)
  }
  if (opt.index == '2') { // Don't use ===
    console.log(opt.index, opt.value)
    opt.next(opt.value)
  }
})
console.log(ret)

Playground Link 游乐场链接

TypeScript doesn't have a way to express the higher-order types needed to give reduce() or forEach() call signatures that allow you to chain functions together like this safely. TypeScript 没有办法表达提供reduce()forEach()调用签名所需的高阶类型,这些调用签名允许您像这样安全地将函数链接在一起。 Neither can you do this with a for loop.你也不能用for循环来做到这一点。 Unless you actually unroll the loop and call arr[2](arr[1](arr[0]())) , you cannot implement this sort of thing hand have the compiler catch you if you make a mistake.除非您实际上展开循环并调用arr[2](arr[1](arr[0]())) ,否则如果您犯了错误,您将无法实现这种事情,让编译器抓住您。 The implementation will therefore involve some amount of assertions where you just tell the compiler that what you're doing is okay.因此,实现将涉及一些断言,您只需告诉编译器您正在做的事情是可以的。

That being said, if you want to have a reusable callChain() function which takes an array of "properly chained functions" as input, you can give that function a call signature which checks the "properness" (propriety?) of the array and which returns the right type.话虽如此,如果您想要一个可重用的callChain() function 以“正确链接的函数”数组作为输入,您可以为 function 提供一个调用签名,以检查数组的“正确性”(适当性?)和它返回正确的类型。 The implementation will be somewhat unsafe, but you only need to write that implementation once, and hopefully call it more than once.该实现会有些不安全,但您只需要编写一次该实现,并希望多次调用它。

Here's one possible way to do that:这是一种可能的方法:

type TupleToArgs<T extends any[]> =
  Extract<[[], ...{ [I in keyof T]: [arg: T[I]] }], Record<keyof T, any>>;
type TupleToChain<T extends any[]> =
  { [I in keyof T]: (...args: TupleToArgs<T>[I]) => T[I] };
type Last<T extends any[]> =
  T extends [...infer _, infer L] ? L : never;

function callChain<T extends any[]>(fns: [...TupleToChain<T>]): Last<T>
function callChain(funcs: ((...args: any) => any)[]) {
  const [f0, ...fs] = funcs;
  return fs.reduce((a, f) => f(a), f0());
}

Before I go into how it works, let's make sure that it works, using your examples:在我 go了解它的工作原理之前,让我们使用您的示例确保它可以正常工作:

const result = callChain(arrayOfFunctions)
// const result: [number, number]

console.log(result) // [1, 1]

callChain([a, b, b]) // error!
// ------------> ~ 
// Type 'B' is not assignable to type '(arg: number) => number'.
// Types of parameters 'str' and 'arg' are incompatible.
// Type 'number' is not assignable to type 'string'.

That's what you want.那正是你想要的。 The proper chain is accepted and the return type is [number, number] .接受正确的链,返回类型为[number, number] And the improper chain results in a compiler error where the first bad element in the chain is highlighted as taking the wrong input type.不正确的链会导致编译器错误,其中链中的第一个坏元素被突出显示为采用错误的输入类型。

So, that's good.所以,这很好。


So, let's explain how it works.所以,让我们解释一下它是如何工作的。 First, the function itself:一、function本身:

function callChain<T extends any[]>(fns: [...TupleToChain<T>]): Last<T>
function callChain(funcs: ((...args: any) => any)[]) {
  const [f0, ...fs] = funcs;
  return fs.reduce((a, f) => f(a), f0());
}

The call signature is generic in T , a tuple type corresponding to the return type of each function in the chain.调用签名在T中是通用的,它是一个元组类型,对应于链中每个 function 的返回类型。 So if T is [string, number, boolean] , that means the first function in the chain returns a string , the second returns a number , and the third returns a boolean .所以如果T[string, number, boolean] ,这意味着链中的第一个 function 返回一个string ,第二个返回一个number ,第三个返回一个boolean The TupleToChain<T> type should therefore turn such a tuple of types into a tuple of functions of the right type.因此, TupleToChain<T>类型应该将这样的类型元组转换为正确类型的函数元组。 The return type of callChain is Last<T> , which should hopefully be just the last element of the T tuple. callChain的返回类型是Last<T> ,它应该是T元组的最后一个元素。

The function is written as a single-call-signature overloaded function . function 被编写为单呼号重载 function Overloaded function implementations are intentionally checked more loosely by TypeScript, (see ms/TS#13235 for why this is), which makes it easier to split apart the function into the strongly-typed call side and the weakly-typed implementation. Overloaded function implementations are intentionally checked more loosely by TypeScript, (see ms/TS#13235 for why this is), which makes it easier to split apart the function into the strongly-typed call side and the weakly-typed implementation. The implementation uses a lot of the any type to sidestep compiler errors.该实现使用了很多any类型来回避编译器错误。 As I said at the outset, the implementation can't really be properly type checked, so I'm using any to stop the compiler from even trying.正如我在一开始所说的那样,实现不能真正进行正确的类型检查,所以我使用any来阻止编译器甚至尝试。

At runtime we're splitting the array into the first element (which takes no input) and the rest of the array (which all take an input), and then using the reduce() array method to do the actual chaining.在运行时,我们将数组拆分为第一个元素(不接受输入)和数组的 rest(都接受输入),然后使用reduce()数组方法进行实际链接。


Let's look at the types, now.现在让我们看看类型。 The easy one is Last :最简单的是Last

type Last<T extends any[]> =
  T extends [...infer _, infer L] ? L : never;

That's just using variadic tuple types and conditional type inference to pluck the last element from a tuple.那只是使用可变元组类型条件类型推断来从元组中提取最后一个元素。

Now for TupleToChain :现在对于TupleToChain

type TupleToChain<T extends any[]> =
  { [I in keyof T]: (...args: TupleToArgs<T>[I]) => T[I] };

This is just a mapped tuple type which takes the bare types in T and produces a new tuple of functions.这只是一个映射元组类型,它采用T中的裸类型并生成一个新的函数元组。 For each type at index I in the input tuple T , the function in the output tuple will return the type T[I] , and it will take as a list of arguments something called TupleToArgs<T>[I] .对于输入元组T中索引I处的每种类型, output 元组中的 function 将返回类型T[I] ,它将作为TupleToArgs<T>[I]的列表So TupleToArgs<T>[I] had better give us the list of arguments required for the I th function in the chain.所以TupleToArgs<T>[I]最好给我们链中第I个 function 所需的 arguments 列表。 If I is 0 , then we want an empty list like [] , whereas if I is J+1 for some J , then we want a list with the single element [T[J]] .如果I0 ,那么我们想要一个类似[]的空列表,而如果I是某个JJ+1 ,那么我们想要一个包含单个元素[T[J]]的列表。

To be painfully clear, if T is [string, number, boolean] , we want TupleToArgs<T>[0] to be [] in order to form ()=>string ;要痛苦地清楚,如果T[string, number, boolean] ,我们希望TupleToArgs<T>[0][]以便形成()=>string we want TupleToArgs<T>[1] to be [string] in order to form (arg: string)=>number ;我们希望TupleToArgs<T>[1][string]以便形成(arg: string)=>number and we want TupleToArgs<T>[2] to be [number] in order to form (arg: number)=>boolean .我们希望TupleToArgs<T>[2][number]以便形成(arg: number)=>boolean

So here's TupleToArgs :所以这里是TupleToArgs

type TupleToArgs<T extends any[]> =
  Extract<[[], ...{ [I in keyof T]: [arg: T[I]] }], Record<keyof T, any>>;

This is another mapped tuple type where we wrap each element of T in a one-tuple (so [string, number, boolean] becomes [[string],[number],[boolean]] ) and then we prepend an empty tuple to it (so we get [[],[string],[number],[boolean]] ).这是另一种映射元组类型,我们将T的每个元素包装在一个元组中(因此[string, number, boolean]变为[[string],[number],[boolean]] ),然后我们在前面添加一个空元组它(所以我们得到[[],[string],[number],[boolean]] )。 This indeed works exactly how we want TupleToArgs to work (the extra [boolean] at the end doesn't hurt anything)...这确实完全符合我们希望TupleToArgs的工作方式(最后的额外[boolean]不会伤害任何东西)......

...but we wrap it in an extra bit Extract<..., Record<keyof T, any>> using the Extract<T, U> utility type . ...但是我们使用Extract<T, U>实用程序类型将它包装在一个额外的位Extract<..., Record<keyof T, any>>中。 This doesn't actually change the type, since the returned tuple will indeed be assignable to Record<keyof T, any> .这实际上并没有改变类型,因为返回的元组确实可以分配给Record<keyof T, any> But it helps the compiler see that for I in keyof T , the type TupleToArgs<T>[I] is valid.但它有助于编译器看到对于I in keyof T ,类型TupleToArgs<T>[I]是有效的。 Otherwise the compiler gets confused and is worried that maybe I is not a key of TupleToArgs<T> .否则编译器会感到困惑并担心I可能不是TupleToArgs<T>的关键。


And that's basically it.基本上就是这样。 It's kind of convoluted, but not the worst thing I've ever written.这有点令人费解,但不是我写过的最糟糕的东西。 I'm sure there are edge cases where it doesn't work.我确信在某些极端情况下它不起作用。 Certainly if your array of functions isn't defined as a tuple type you'll have a bad time.当然,如果您的函数数组未定义为元组类型,您将度过一段糟糕的时光。 For a single chain like [a, b, c] , there's really nothing better than just c(b(a())) .对于像[a, b, c]这样的单个链,没有什么比c(b(a()))更好的了。 But if you find yourself needing to write such chains many times, something like callChain() be worth its complexity.但是如果你发现自己需要多次编写这样的链,那么像callChain()这样的东西就值得它的复杂性了。 Exactly where the line where that happens is subjective;发生这种情况的确切位置是主观的; in my opinion you'd have to be doing these tuple-typed function chains quite often in your code base for it to be worthwhile.在我看来,你必须经常在你的代码库中使用这些元组类型的 function 链,这样才值得。 But whether or not you actually use this, it's interesting to consider how close the language can get you to representing such operations in the type system.但是无论您是否实际使用它,有趣的是考虑该语言可以使您在类型系统中表示此类操作有多接近。

Playground link to code Playground 代码链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM