如何调试递归 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>

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

链接到带有代码的打字稿游乐场。

相关问题:

  • 类型的打字稿递归子集
  • 打字稿:为什么不能将"函数"分配给"(...参数: 任何[]) => 任何'?

为了完整性,包括映射函数(函数有效,但键入是部分的,尚未工作):

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
}

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

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

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

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

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

让我们修改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属性不会扩展到StateAppStateTypedFunction<State>。 也就是说,(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。 但我认为主要问题已经回答了,所以我就到此为止。

希望有帮助;祝你好运!

链接到代码

最新更新