如何调试递归 TypeScript 泛型类型

AJP*_*AJP 1 typescript

给定以下架构和实例:

interface SubState {
    value: number
}

interface AppState {
    subState: SubState
}

const state: AppState = {
    subState: { value: 42 },
}
Run Code Online (Sandbox Code Playgroud)

我有一个嵌套对象,其中包含使用该state实例或其子部分的函数(Redux 选择器):

const selectors = {
    subState: { isPositive: (state: SubState) => state.value > 0 },
}
Run Code Online (Sandbox Code Playgroud)

实际的选择器对象有几层深,并且在状态树的不同级别有数十个函数。

我已将选择器对象转换为以下类型(使用最后显示的函数)。每个键都会被迭代,如果它是一个函数,那么它会被替换为一个采用顶级状态的函数AppState,找到该函数的正确子状态并用它调用它。所以签名全部从: 转换(SpecificSubState) => ReturnType(AppState) => ReturnType

const mappedSelectors = {
    subState: { isPositive: (state: AppState) => true },
}
Run Code Online (Sandbox Code Playgroud)

我希望为映射函数的返回值提供一个工作稳健的动态类型。我尝试使用以下实现,但它还不起作用:

interface TypedFunction<T> extends Function {
    (state: T): any;
}
type RecursivePicker<Sel, State> = { 
    [K in keyof Sel]: Sel[K] extends TypedFunction<State>
        ? ((state: AppState) => ReturnType<Sel[K]>)
        : (
            Sel[K] extends object
            ? RecursivePicker<Sel[K], State>
            : never
            // never
        )
}

const mappedSelectors: RecursivePicker<typeof selectors, AppState> = {
    subState: { isPositive: (state: AppState) => true },
}

// errors with:
//   Cannot invoke an expression whose type lacks a call signature.
//   Type 'RecursivePicker<(state: SubState) => boolean, AppState>' has no compatible call signatures.
mappedSelectors.subState.isPositive(state)

type ExpectedTypeManual = (state: AppState) => true
type MutuallyExtends<T extends U, U extends V, V=T> = true
// errors with:
//   Type 'RecursivePicker<(state: SubState) => boolean, AppState>' does not satisfy the constraint 'ExpectedTypeManual'.
//   Type 'RecursivePicker<(state: SubState) => boolean, AppState>' provides no match for the signature '(state: AppState): true'.
type ShouldBeNoErrorHere = MutuallyExtends<typeof mappedSelectors.subState.isPositive, ExpectedTypeManual>
Run Code Online (Sandbox Code Playgroud)

我不确定问题出在哪里。关于如何进一步调试有什么建议吗?

使用代码链接到 Typescript Playground

相关问题:

为了完整性而包含的映射函数(函数可以工作,但打字是部分的并且尚未工作):

function mapSelectors<Sel, State, SubState> (selectors: Sel, stateGetter: (state: State) => SubState) {

  const mappedSelectors = Object.keys(selectors).reduce((innerMappedSelectors, selectorOrSelectorGroupName) => {

    const value = selectors[selectorOrSelectorGroupName]
    if (typeof value === "function") {
      innerMappedSelectors[selectorOrSelectorGroupName] = (state: State) => value(stateGetter(state))
    } else {
      function getSubState (state: State) {
        return stateGetter(state)[selectorOrSelectorGroupName]
      }
      innerMappedSelectors[selectorOrSelectorGroupName] = mapSelectors(value, getSubState)
    }

    return innerMappedSelectors
  }, {} as {[selectorName in keyof typeof selectors]: (state: State) => ReturnType<typeof selectors[selectorName]>})
  // }, {} as {[selectorName in keyof typeof gameSelectors]: (state: ApplicationState) => ReturnType<typeof gameSelectors[selectorName]>}),

  return mappedSelectors
}
Run Code Online (Sandbox Code Playgroud)

jca*_*alz 5

因此,显示类型mappedSelectors无济于事

const mappedSelectors: RecursivePicker<{
    subState: {
        isPositive: (state: SubState) => boolean;
    };
}, AppState>
Run Code Online (Sandbox Code Playgroud)

当我有一个复杂类型,其 IntelliSense 信息不透明并且充满其他类型名称时,我一直使用的一种技术是说服编译器使用以下类型别名扩展属性名称:

type Prettify<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
Run Code Online (Sandbox Code Playgroud)

这并没有真正改变类型(可能存在边缘情况,但它主要只是强制编译器遍历对象的属性并将它们写出来),但它通常会提高类型的可读性。

让我们修改一下RecursivePicker来使用它:

type RecursivePicker<Sel, State> = Prettify<
  {
    [K in keyof Sel]: Sel[K] extends TypedFunction<State>
      ? ((state: AppState) => ReturnType<Sel[K]>)
      : (Sel[K] extends object ? RecursivePicker<Sel[K], State> : never)
        // never
  }
>;
Run Code Online (Sandbox Code Playgroud)

现在我们看看mappedSelectors

const mappedSelectors: {
    subState: {
        isPositive: {};
    };
}
Run Code Online (Sandbox Code Playgroud)

现在是一个透明类型,并且透明不是你想要的。为什么不?让我们清除所有其他内容,看看会发生什么isPositive

type IsPositive = RecursivePicker<
  { isPositive: (x: SubState) => boolean },
  AppState
>;
// type IsPositive = { isPositive: {}; }
Run Code Online (Sandbox Code Playgroud)

果然,就变成了{ isPositive: {} }。这意味着(x: SubState) => boolean的属性不会延伸到 的TypedFunction<State>位置。也就是说,不延伸。 也许您想使用? 而不是?StateAppState(x: Substate) => boolean(x: AppState) => anyRecursivePicker<..., AppState>RecursivePicker<..., SubState>

const mappedSelectors: RecursivePicker<typeof selectors, SubState> = {
  subState: { isPositive: (state: AppState) => true }
};
// const mappedSelectors: {
//    subState: {
//        isPositive: (state: AppState) => boolean;
//    };
// }
Run Code Online (Sandbox Code Playgroud)

现在它看起来很像您想要的类型,您可以调用它:

mappedSelectors.subState.isPositive(state); // okay
Run Code Online (Sandbox Code Playgroud)

您的预期类型仍然不完全正确,因为编译器将返回值视为booleanand not true。只要boolean没问题,可以执行以下操作:

type ExpectedTypeManual = (state: AppState) => boolean;
type MutuallyExtends<T extends U, U extends V, V = T> = true;
type ShouldBeNoErrorHere = MutuallyExtends<
  typeof mappedSelectors.subState.isPositive,
  ExpectedTypeManual
>; // okay
Run Code Online (Sandbox Code Playgroud)

这可能是你的主要问题。


{}那你之前为什么得到呢?如果属性是与您期望的类型不匹配的函数,它将继续到下一个子句,即Sel[K] extends object ? .... 但函数确实可以扩展object(它们不是基元):

type FunctionExtendsObject = (() => boolean) extends object ? true : false; // true
Run Code Online (Sandbox Code Playgroud)

因此,它(state: SubState) => boolean被映射了,并且它没有任何可映射属性(它确实有属性,但当您检查类型时 TypeScript 会忽略它们)

type NoMappableKeys = keyof (() => boolean); // never
Run Code Online (Sandbox Code Playgroud)

所以你最终{}会出来。

当对象的函数属性与您的预期类型不匹配时,您想做什么?应该不修改吗?该财产是否应该被移除?你应该得到一个编译错误吗?是否应该改成完全不同的东西?您需要做出决定,然后可能进行修改RecursivePicker。但我认为主要问题已经得到解答,所以我就到此为止。

希望有帮助;祝你好运!

链接到代码