如何调试递归TypeScript通用类型

时间:2019-07-14 09:41:37

标签: typescript

给出以下架构和实例:

interface SubState {
    value: number
}

interface AppState {
    subState: SubState
}

const state: AppState = {
    subState: { value: 42 },
}

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

const selectors = {
    subState: { isPositive: (state: SubState) => state.value > 0 },
}

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

我已经将选择器对象转换为以下类型(使用末尾显示的函数)。每个键都进行迭代,如果它是一个函数,则将其替换为具有顶级状态AppState,为该函数找到正确的子状态并对其进行调用的函数。因此,签名全部从(SpecificSubState) => ReturnType转换为(AppState) => ReturnType

const mappedSelectors = {
    subState: { isPositive: (state: AppState) => true },
}

我想为映射函数的返回值提供一个健壮的动态类型。我尝试使用以下实现,但仍无法使用:

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>

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

Link to Typescript playground with code

相关问题:

为完整起见,包含了映射功能(功能可以正常工作,但键入是部分操作,尚不可用):

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
}

1 个答案:

答案 0 :(得分:1)

因此,mappedSelectors的类型无益地显示为

const mappedSelectors: RecursivePicker<{
    subState: {
        isPositive: (state: SubState) => boolean;
    };
}, AppState>

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

type Prettify<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;

并不会真正改变类型(可能存在边缘情况,但是大多数情况下,它只是强制编译器遍历对象的属性并将其写出),但通常可以提高类型的可读性。 / p>

让我们修改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
  }
>;

现在,我们看看mappedSelectors并看到:

const mappedSelectors: {
    subState: {
        isPositive: {};
    };
}

现在是透明类型,显然不是您想要的。为什么不?让我们清除所有其他内容,看看isPositive会发生什么:

type IsPositive = RecursivePicker<
  { isPositive: (x: SubState) => boolean },
  AppState
>;
// type IsPositive = { isPositive: {}; }

当然,它变成{ isPositive: {} }。这意味着的(x: SubState) => boolean属性不会扩展TypedFunction<State>,其中StateAppState。也就是说,(x: Substate) => boolean不扩展(x: AppState) => any也许您想使用RecursivePicker<..., AppState>而不是RecursivePicker<..., SubState>

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

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

mappedSelectors.subState.isPositive(state); // okay

您的期望类型仍然不完全正确,因为编译器将返回值视为boolean而不是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

这可能是您的主要问题。


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

type FunctionExtendsObject = (() => boolean) extends object ? true : false; // true

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

type NoMappableKeys = keyof (() => boolean); // never

因此,您最终得到{}的出现。

当对象的功能属性与您的预期类型不匹配时,您想要做什么?应该不做修改吗?是否应删除财产?你应该得到一个编译错误?是否应该将其更改为完全不同的内容?您需要做出决定,然后可能需要修改RecursivePicker。但是我认为主要问题已经回答,所以我就到此为止。

希望有所帮助;祝你好运!

Link to code