我想编写一个泛型 Select 器和映射器方法.

interface State {
    user: {
        name: string;
        age: number;
    }
}

const selectName = (state: State) => state.user.name;
const selectAge = (state: State) => state.user.age;

const state: State = { user: { name: 'John Doe', age: 38 } };

现在我想要一个在类型脚本中的泛型方法,它将为我提供以下内容,而不使用任何或对象.

const selector = buildSelector(selectName, selectAge, (name, age) => `${name} - ${age}`);
  1. BuildSelector可以获得...args个带有签名(STATE:STATE)=>可以返回任何内容的 Select 器.
  2. 最后一个参数将是一个映射器函数,它将自动知道它将接收的类型

推荐答案

以下是我如何写出buildSelector():

function buildSelector<T extends any[], U>(...args: [
  ...selectors: { [I in keyof T]: (state: State) => T[I] },
  callback: (...args: T) => U
]): (state: State) => U {
  const selectors = args.slice(0, -1) as ((state: State) => any)[];
  const callback = args.at(-1) as (...args: any) => U;
  return (state) => callback(...selectors.map(f => f(state)))
}

调用签名相当复杂,主要是因为我们需要表示我们希望在单个回调函数中接收任意数量的 Select 器followed by.这就好比使用一个rest parameter,然后在后面加上一个额外的参数.但JavaScript只支持将REST参数放在参数列表的第end位.因此,从概念上讲,该函数如下所示

// not valid TS nor valid JS, do not use:
function buildSelector<T extends any[], U>(
  ...selectors: { [I in keyof T]: (state: State) => T[I] },
  callback: (...args: T) => U
): (state: State) => U {
  return (state) => callback(...selectors.map(f => f(state)))
}

让我们先看一下假设的版本,然后再讨论现实世界的版本.该函数是T中的generic,这是callback的参数的tuple type,以及selectors数组的return types的元组.因此,selectors的类型是mapped tuple type除以T,因此selectors的第I个元素是类型(state: State) => T[I],这是一个接受State的函数,它返回T的第I个元素.在返回类型callbackU中,buildSelector也是泛型的.

假设的实现也相当简单:它是一个接受state并在selectorsmappped版本上调用callback的函数,其中selectors的每个元素都以state作为参数进行调用.请注意,当对元组使用map()时,编译器不可能遵循所涉及的逻辑,请参见Mapping tuple-typed value to different tuple-typed value without casts.因此,在现实版本中,实现将需要大约type assertions个元素,在我们的假设世界中,这并不是必需的,因为在我们的假设世界中,领先的REST元素在JavaScript中是被接受的.


好了,现在回到现实世界:

function buildSelector<T extends any[], U>(...args: [
  ...selectors: { [I in keyof T]: (state: State) => T[I] },
  callback: (...args: T) => U
]): (state: State) => U {
  const selectors = args.slice(0, -1) as ((state: State) => any)[];
  const callback = args.at(-1) as (...args: any) => U;
  return (state) => callback(...selectors.map(f => f(state)))
}

由于参数列表可以是可变长度的,变量部分在开头,我们别无 Select ,只能在开头给函数一个args个REST参数.虽然在JavaScript中不支持前导REST参数,但幸运的是,有is个支持leading rest tuple element英寸打字的脚本.因此,args的类型是一个可变元组,具有前导的REST selectors元素和最后的callback元素.它们的类型与以前相同.

在函数实现内部,我们需要将args分解为selectorscallback.我们不能用像const [...selectors, callback] = args这样的destructuring assignment来做到这一点,因为在那里,JavaScript也不支持领先的REST元素.所以我们使用像slice()at()这样的数组方法.当然,你可以随心所欲地go 做.

我使用断言和the any type来赋予selectorscallback足够宽松的类型,这样编译器就不会抱怨return (state) => callback(...selectors.map(f => f(state))).如前所述,我们需要它们,因为编译器无法理解selectors.map(f => f(state))将返回类型T的值.但我们也需要它们,因为编译器无法理解at(-1)将生成最终的回调函数类型(而不是 Select 器类型),而slice(0, -1)将生成 Select 器类型的元组(而不是最终的回调类型).也就是说,分割和拆分元组类型的数组不会产生元组类型.有一个功能请求支持负指数的强类型,atmicrosoft/TypeScript#47660,但被拒绝了,因为太复杂了.因此,您在这里所能做的最好的事情就是断言实现是正确的,并放弃对编译器的大量验证.


好吧,这就是我的解释.让我们看看它是否奏效:

const selector = buildSelector(
  selectName, selectAge, (name, age) => `${name} - ${age}`
);

const x = selector(state);
// const x: string
console.log(x) // "John Doe - 38" 

看上go 不错!编译器contextually infers将最终回调的nameage参数类型分别为stringnumber.它还理解selector(state: State)=>string类型,因为回调返回类型是string.您可以看到,该实现也能按预期工作.

Playground link to code

Typescript相关问答推荐

创建一个将任意命名类型映射到其他类型的TypScript通用类型

如何将http上下文附加到angular中翻译模块发送的请求

用泛型类型覆盖父类函数导致类型y中的Property x不能赋给基类型Parent中的相同属性."'''''' "

如何创建泛型类型组件来使用React Native的FlatList或SectionList?'

如何在TypeScrip面向对象程序设计中用方法/属性有条件地扩展类

如何将类型从变量参数转换为返回中的不同形状?

具有动态键的泛型类型

具有泛型类方法的类方法修饰符

使某些(嵌套)属性成为可选属性

Angular material 无法显示通过API获取的数据

自动通用回调

如何避免从引用<;字符串&>到引用<;字符串|未定义&>;的不安全赋值?

打字脚本中的模块化或命名空间

基于未定义属性的条件属性

基于闭包类型缩小泛型类型脚本函数的范围

通过辅助函数获取嵌套属性时保留类型

Karma Angular Unit测试如何创建未解析的promise并在单个测试中解决它,以判断当时的代码是否只在解决后运行

无法使用prisma中的事务更新记录

在类型{}上找不到带有string类型参数的索引签名 - TypeScript

如何在由函数参数推断的记录类型中具有多个对象字段类型