如何使通用可变参数函数中先前声明的函数的返回类型成功进行上下文推断?

Posted

技术标签:

【中文标题】如何使通用可变参数函数中先前声明的函数的返回类型成功进行上下文推断?【英文标题】:How to make contextual inference succeed for the return type of previously declared function in a generic variadic function? 【发布时间】:2022-01-23 02:54:09 【问题描述】:

Playground

我有一个案例,对于先前声明的泛型函数,上下文推断失败。我不明白为什么它会失败,但是提供一个没有泛型参数的函数可以成功地根据上下文推断出来。

我有一个限制,即参数projector 必须排在最后。这是输入NgRx的createSelector函数:https://github.com/ngrx/platform/issues/3268。

const state:  counter: number  =  counter: 0 ;

const getSame = <T>(same: T): T => same;
const returnState = () => state;

declare function createSelector<Slices extends unknown[], U>(...args: [...getSlices:  [i in keyof Slices]: () => Slices[i] , projector: (...args: Slices) => U]): (...args: unknown[]) => U;

const variadicSlicesWithContextualInference = createSelector(returnState, d => d);
const test1 = variadicSlicesWithContextualInference(state).counter;

const variadicSlicesWithContextualInferenceOfGeneric = createSelector(returnState, <T>(d: T): T => d);
// ???? inference fails, variadicSlicesWithContextualInferenceOfGeneric(state) is unknown
const test2 = variadicSlicesWithContextualInferenceOfGeneric(state).counter;

const variadicSlicesWithContextualInferenceOfDeclaredFn = createSelector(returnState, getSame);
// ???? inference fails, variadicSlicesWithContextualInferenceOfDeclaredFn(state) is unknown
const test3 = variadicSlicesWithContextualInferenceOfDeclaredFn(state).counter;

// But if I declare a function specific for one "slice", then inference works properly
declare function createSelectorOneSlice<T, U>(...args: [getSlice: (...args: unknown[]) => T, projector: (...args: [T]) => U]): (...args: unknown[]) => U;

const oneSlice = createSelectorOneSlice(returnState, getSame);
const test4 = oneSlice(state).counter;

【问题讨论】:

test2 应该显示什么?它没有使用variadicSlicesWithContextualInferenceOfGeneric。 TypeScript 的推理没有完整的统一算法,因此不能保证像这样的高阶类型杂耍有效。编译器最好在末尾添加其余元素,例如this;那对你有用吗?虽然,正如所问的那样,您的问题是“为什么会发生这种情况”所以也许您对解决方法不感兴趣...... 感谢您发现该错误。我已更新 test2 以使用该功能。似乎无法推断,这意味着 TS 由于某种原因无法推断T。查看您的解决方案,似乎使用 args 的元组类型是我的问题。不幸的是,我正在寻找一个保留元组类型的解决方案,因为它需要实现 createSelector 的参数类型我需要 github.com/ngrx/platform/issues/3268 您的主要问题是“为什么这不起作用”还是“我怎样才能让它起作用”?如果是后者,那么回答的人应该花费大部分时间来尝试获得该签名的工作版本;如果是前者,那么他们应该花时间尝试识别和解释 TS 启发式推理算法如何无法按预期运行。 我希望上下文推断起作用,所以我更新了我的问题标题。 我还添加了projector 必须在函数参数中排在最后的限制,以维护createSelector 的当前API。 【参考方案1】:

我怀疑这在 TypeScript 中目前是不可能的。推理算法不是microsoft/TypeScript#30134 中要求/讨论的“完全统一”;相反,它在本质上更具启发性,虽然它经常做人们想做的事,但也有局限性。

您在这里使用leading elements in tuple types,这似乎使事情变得复杂。如果您能够重新排序参数以使可变参数部分出现在最后(就像行为良好的rest parameter 应该):

declare function createSelector<S extends unknown[], U>(...args: [
    projector: (...args: S) => U,
    ...getSlices:  [I in keyof S]: () => S[I] 
]): (...args: unknown[]) => U;

或等效

declare function createSelector<S extends unknown[], U>(
    projector: (...args: S) => U,
    ...getSlices:  [I in keyof S]: () => S[I] 
): (...args: unknown[]) => U;

   

那么事情可能会更好:

const variadicSlicesWithContextualInferenceOfDeclaredFn = createSelector(getSame, returnState);
variadicSlicesWithContextualInferenceOfDeclaredFn(state).counter; // okay
 

很遗憾,您不能这样做。


由于您的可变参数版本适用于非通用 getSlices 元素,我认为您最好的选择是一种混合方法,您可以为某些非可变参数 getSlices 大小声明一系列 overloaded 调用签名,捕捉最可能的用例。

当开发者调用createSelector()时,一般会传递多少个参数?大概只有几个。因此,您可以处理这几个以获得所需的推理,然后回退到可变参数版本:

// 0
declare function createSelector<U>(
  projector: () => U): (...args: unknown[]) => U;

// 1
declare function createSelector<A, U>(
  getSliceA: () => A, 
projector: (a: A) => U): (...args: unknown[]) => U;

// 2
declare function createSelector<A, B, U>(
  getSliceA: () => A, 
  getSliceB: () => B, 
projector: (a: A, b: B) => U): (...args: unknown[]) => U;

// 3
declare function createSelector<A, B, C, U>(
  getSliceA: () => A, 
  getSliceB: () => B, 
  getSliceC: () => C, 
projector: (a: A, b: B, c: C) => U): (...args: unknown[]) => U;

// n
declare function createSelector<S extends unknown[], U>(...args: [
    ...getSlices:  [I in keyof S]: () => S[I] ,
    projector: (...args: S) => U]
): (...args: unknown[]) => U;

这也有效:

const variadicSlicesWithContextualInferenceOfDeclaredFn = createSelector(returnState, getSame);
variadicSlicesWithContextualInferenceOfDeclaredFn(state).counter; // okay

所以,万岁!


重载确实有缺点;例如,当您不直接调用重载函数,而是试图让编译器推断一些关于重载函数类型的信息时,它往往会忽略除第一个或最后一个之外的所有内容调用签名,这可能会令人困惑和令人惊讶。 (请参阅inferring in conditional types 的文档,其中讨论了重载)。在这种情况下,重载将是多余的,维护起来更烦人。但是,如果您关心调用者的通用 getSlice 元素是否正确推断,那么重载是我能想到的最好的实现方式。

Playground link to code

【讨论】:

以上是关于如何使通用可变参数函数中先前声明的函数的返回类型成功进行上下文推断?的主要内容,如果未能解决你的问题,请参考以下文章

第21课 可变参数模板_展开参数包

C语言中如何实现可变参函数

函数和包

ref out params

如何对可变参数模板函数的异构参数包进行通用计算?

学习 Python 之 函数