TypeScript奇怪地连接类型

ecfdbz9o  于 2022-11-18  发布在  TypeScript
关注(0)|答案(1)|浏览(115)

假设我有两个不同的函数,我想根据传递的函数名调用它们,同时,我想为函数参数添加类型注解。
最小示例:

const a = (options: { o1: number }) => {}
const b = (options: { o2: string }) => {}

const functions = { a, b }

type A = {
  name: 'a'
  options: Parameters<typeof a>[0]
}
type B = {
  name: 'b'
  options: Parameters<typeof b>[0]
}

type Data = A | B

(data: Data) => {
  functions[data.name](data.options)
}

但是TypeScript不允许传递data.options

TS2345: Argument of type '{ o1: number; } | { o2: string; }' is not assignable to parameter of type '{ o1: number; } & { o2: string; }'.   Type '{ o1: number; }' is not assignable to type '{ o1: number; } & { o2: string; }'.     Property 'o2' is missing in type '{ o1: number; }' but required in type '{ o2: string; }'.

看起来像TypeScript转换

type Data = A | B

进入

type Data = {
  name: 'a' | 'b'
  options: Parameters<typeof a>[0] | Parameters<typeof b>[0]
}

这对我来说似乎是不正确的

v8wbuo2f

v8wbuo2f1#

这里的问题我称之为“相关联合类型”,这在microsoft/TypeScript#30581中报告过,并通过microsoft/TypeScript#47109得到了解决,microsoft/TypeScript#47109建议进行某种重构,以帮助编译器理解您正在做什么。
通常,如果有一个函数f和一个参数x,两者都是联合类型,如下所示:

declare const f: ((options: { o1: number }) => void) | ((options: { o2: string }) => void)
declare const x: { o1: number } | { o2: string }

则使用该参数调用该函数是不安全的,TypeScript编译器将生成一个错误,告诉您:

f(x); // error!
// Argument of type '{ o1: number; } | { o2: string; }' is not assignable to 
// parameter of type '{ o1: number; } & { o2: string; }'.

该错误消息可能会令人困惑,但它只是说明参数联合不起作用;它所知道的唯一可行的方法是参数交集,这是根据TS3.3中引入并在microsoft/TypeScript#29011中实现的对调用联合类型的支持。
这将是一个错误,这是有道理的,因为就编译器所知,f可能是(options: { o1: number }) => void)类型,而x{ o2: string }类型。也就是说,它们可能不匹配。
现在,在您的程式码中,您要执行下列动作:

const f = functions[data.name];
const x = data.options;
f(x); // same error!

编译器会产生与上述相同的错误,原因也相同:fx都是联合类型,就编译器所知,它们可能不匹配。但是编译器没有意识到fx的类型是相互关联的。它们都依赖于data的类型,这样就不可能出现不匹配。
这实际上是TypeScript的一个限制。如果您保留联合类型的fx,编译器将无法看到它们之间的任何关联。
microsoft/TypeScript#47109中推荐的重构方法是使函数成为generic而不是联合,并向其传递相应泛型类型的参数。为了使其正常工作,必须非常小心地管理函数和参数类型。
首先,我们可以创建一个DataMapMap接口,它表示一个基本的键-值关系,我们可以从这个关系构建其他类型:

interface DataMap {
  a: { o1: number },
  b: { o2: string }
};

然后,我们可以将functions变量注解为mapped type,它显式迭代DataMap的键和值类型:

const a = (options: { o1: number }) => { }
const b = (options: { o2: string }) => { }   
const functions: { [K in keyof DataMap]: (options: DataMap[K]) => void } =
  { a, b }

Data不是硬编码的AB的直接并集,我们可以将Data重写为通用的 * 分布式对象类型 *(如ms/TS#47109中所述,它是我们直接将index

type Data<K extends keyof DataMap = keyof DataMap> =
  { [P in K]: { name: P, options: DataMap[P] } }[K]

根据需要,我们可以定义AB

type A = Data<"a">
type B = Data<"b">

如果需要,可以使用不带类型参数的Data类型(由于它的泛型默认值),并查看它是否与您的版本是相同的类型:

type D = Data
/* type D = {
    name: "a"; options: { o1: number; };
} | {
    name: "b"; options: { o2: string; };
} */
  • 现在 * 我们可以编写您的函数;对于一般的K,我们接受Data<K>,而不是接受Data
<K extends keyof DataMap>(data: Data<K>) => {
  functions[data.name](data.options); // okay
}

是的,没有编译器错误!这是因为编译器将functions[data.name]data.options类型视为相应的相关泛型:

<K extends keyof DataMap>(data: Data<K>) => {
  const f: (options: DataMap[K]) => void = functions[data.name];
  const x: DataMap[K] = data.options;
  f(x); // okay
}

你可以看到参数x的类型是DataMap[K],函数f的类型是(options: DataMap[K]) => void,所以这个参数就是函数接受的类型,编译器不再出错。
Playground代码链接

相关问题