从打字稿中引用自身的父对象推断窄类型

Infer narrow type from parent object referencing itself in typescript

我想像这样约束对象内部的回调函数的参数类型:

makeObj({
  items: [
    {
      value: "foo",
      func(items) {}
    },
    {
      value: "bar",
      func(items) {}
    }
  ]
});

我想将 func 的参数 items 约束为 "foo" | "bar"value 的值可以是任何字符串。

我已经有了这个,但显然不起作用:

interface MyObject<T extends string, Values extends readonly MyObject<T, Values>[]> {
  value: T;
  func: (items: Values) => void;
}
interface Items<T extends string, Data extends readonly MyObject<T, Data>[]> {
  items: readonly [...Data];
}
function makeObj<T extends string, Data extends readonly MyObject<T, Data>[]>(opt: Items<T, Data>) {
  return opt;
}

makeObj({
  items: [
    {
      value: "foo",
      func(items) {}
/* Error on "func":
Type '(items: MyObject<string, unknown>) => void' is not assignable to type '(items: unknown) => void'.
  Types of parameters 'items' and 'items' are incompatible.
    Type 'unknown' is not assignable to type 'MyObject<string, unknown>'.
*/
    },
    {
      value: "bar",
      func(items) {} // same error on "func"
    }
  ]
});

Typescript Playground


一些背景信息: 我正在编写一个程序,其中定义了一些“动作”。每个动作都是一个对象中的一个函数来描述这个函数(给它一个名字、描述、输入类型、输出类型、次级输入类型)。辅助输入依赖于用户提供的数据。此数据的每一项都有一个名称和定义的类型。它的值由最终用户提供。根据其他项目的价值,一个项目是否可以显示给用户。最后一部分由相关回调函数控制。
我已经使用窄类型推断设置了所有这些,以便在编写每个操作时不会犯任何错误,但是无论回调函数具有哪种类型,这种窄类型推断都会失效。要么是因为它不能再推断出窄类型,要么是因为我收到类型错误(例如,当我使用 object 作为参数的类型时(我最终想使用一个对象作为参数,而不仅仅是一个字符串)). 这种窄类型推断是这样工作的:


编辑:

我想要一个解决方案,其中我没有将类型嵌入到函数参数中,而是作为一个 interface/type 可以参考,例如:

interface Magic<MagicParam> {
  items: MagicParam;
}
makeObj<MagicParam>(opt: Magic<MagicParam>) {
  return opt;
}

Magic 可以是接口或类型,可以有任意数量的类型参数。 makeObj 也可以有任意数量的类型参数。

您的 makeObj() 函数可能 generic in the union of string literal types 对应于函数参数 items 属性 的 value 属性。如果我们调用泛型类型参数 K,并且如果您传入 {items: [{value: "x"},{value: "y"},{value: "z"}]}(暂时忽略 func),那么 K 应该是 "x" | "y" | "z"

那么你可以用K来表达对makeObj()的论证。如果我们称那个类型为Opt<K>,我们可以这样写:

interface Opt<K extends string> {
  items: Array<{ value: K, func: (items: K) => void }>
}

这意味着,给定 value 属性的 K,我们希望 Opt<K>items 属性 是一个数组,其中的元素该数组是具有类型 Kvalue 属性 和输入类型为 K.[=48 的 func 回调 属性 的对象=]

让我们通过评估 Opt<"foo" | "bar">(并使用 conditional type inference and a mapped type 诱使编译器显示结构的详细信息)来确保这就是您想要的:

type Test = Opt<"foo" | "bar"> extends
  infer O ? { [K in keyof O]: O[K] } : never 

/* type Test = {
    items: {
        value: "foo" | "bar";
        func: (items: "foo" | "bar") => void;
    }[];
} */

看起来不错。


无论如何,如上所述,makeObjK中将是通用的,并接受类型为Opt<K>的参数:

function makeObj<K extends string>(opt: Opt<K>) {
  return opt;
}

让我们看看它是否有效:

const obj = makeObj({
  items: [
    {
      value: "foo",
      func(items) {
        // (parameter) items: "foo" | "bar"
      }
    },
    {
      value: "bar",
      func(items) {
        // (parameter) items: "foo" | "bar"
      }
    }
  ]
});
// const obj: Opt<"foo" | "bar">

看起来也不错。编译器推断 K"foo" | "bar",然后 contextually types func 属性 的 items 回调参数也是 "foo" | "bar".并且出现的 obj 是所需的 Opt<"foo" | "bar"> 类型。

Playground link to code