【问题标题】:Get Narrow Type from Object of Methods从方法对象获取窄类型
【发布时间】:2021-12-02 08:38:56
【问题描述】:

我想得到一个像这样的 Narrowing 类型:

type Expected = {
    method: 'firstNamespace/firstMethod'
    payload: [string]
} | {
    method: 'firstNamespace/secondMethod'
    payload: [number, number]
} | {
    method: 'secondNamespace/firstMethod'
    payload: [number]
}

来自这样的方法的对象:

const Methods = {
    firstNamespace: {
        firstMethod: (p: string) => {},
        secondMethod: (p: number, k: number) => {}
    },
    secondNamespace: {
        firstMethod: (p: number) => {}
    }
}

我尝试了各种方法,但我认为我在 TypeScript 中存在一些误解。

我已经使用 TypeScript 将近一年了,任何超出基本类型的东西似乎都超出了我的能力范围......

【问题讨论】:

  • 你真的经历过吗:typescriptlang.org/docs/handbook/2/types-from-types.html有很多。
  • 您是否希望处理任意嵌套的Methods-like 结构? this code 是否满足您的需求?如果是这样,我可以写一个答案;如果没有,请edit 你的例子有一些失败的用例。
  • 这正是我所需要的,非常感谢@jcalz!我会尽力了解它是如何工作的。

标签: javascript typescript object types narrowing


【解决方案1】:

这可以在 TypeScript 中通过 recursive conditional types 深入了解 Methods 的嵌套属性,以及 template literal types 在类型级别将方法名称连接在一起。

让我们将您要查找的类型函数称为MethodsToExpected<T>,它采用对象类型T 并生成对象类型的union,其method 属性是每个方法的"/" 分隔路径名称,其payload 属性是相应方法的参数类型的tuple。然后我们可以像这样递归地定义MethodsToExpected<T>

type MethodsToExpected<T> = { [K in keyof T]-?:
    T[K] extends (...args: infer P) => any ? { method: K, payload: P } :
    MethodsToExpected<T[K]> extends infer X ? (
        X extends { method: infer M, payload: infer P } ? (
            { method: `${Extract<K, string>}/${Extract<M, string>}`; payload: P }
        ) : never
    ) : never
}[keyof T]

构造 {[K in keyof T]-?: XXX}[keyof T],它立即将 indexes intomapped type 及其键,生成所有 XXX 类型的联合。

对于T 中的每个属性键K,我们检查属性类型T[K]。如果它是一个函数类型,那么我们获取它的参数列表并立即返回{method: K, payload: P}。否则,我们将MethodsToExpected&lt;T[K]&gt; 应用于该属性并对其进行检查。

(旁白:我正在使用conditional type inferenceMethodsToExpected&lt;T[K]&gt;“复制”到一个新的类型参数X 中,然后我将其用作distributive conditional type 中的检查类型,以便X 中的任何联合被分配到最终的联合。如果你只是使用MethodsToExpected&lt;T[K]&gt; extends {method: infer M, payload: infer P} 而不是中间的X,它会产生像{method: "a/b" | "a/c", payload: [string] | [number]} 这样的东西而不是所需的{method: "a/b", payload: [string]} | {method: "a/c", payload: [number]}。)

对于MethodsToExpected&lt;T[K]&gt;的每个联合元素,我们拉出method类型Mpayload类型P,并构建一个新的method/payload对。 payload 类型不会改变,只是 P,但我们将当前键 K 和斜线 "/" 附加到具有模板文字类型的 M 之前。编译器不能确定KM 都是string 类型,所以它不想让你直接写`${K}/${M}`。相反,我们使用the Extract&lt;T, U&gt; utility type 来让编译器相信我们只会连接strings。


让我们看看它是否有效:

type Expected = MethodsToExpected<typeof Methods>;
/* type Expected = {
    method: "firstNamespace/firstMethod";
    payload: [p: string];
} | {
    method: "firstNamespace/secondMethod";
    payload: [p: number, k: number];
} | {
    method: "secondNamespace/firstMethod";
    payload: [p: number];
} */

看起来不错。为了确保它深入到嵌套的子属性,让我们尝试一个不同的:

type Nested = MethodsToExpected<{ a: { b: { c: { d: { e: (f: string) => number } } } } }>;
/* type Nested = {
    method: "a/b/c/d/e";
    payload: [f: string];
} */

也不错。

Playground link to code

【讨论】:

  • 这太棒了。
猜你喜欢
  • 2013-08-29
  • 2017-06-29
  • 1970-01-01
  • 1970-01-01
  • 2020-05-22
  • 1970-01-01
  • 1970-01-01
  • 2021-09-26
  • 1970-01-01
相关资源
最近更新 更多