我正在开发一个打字脚本项目,我遇到了这样一种情况,我想对函数的第二个参数data进行类型推断和完成,该函数依赖于函数的第一个参数type.

以下是示例类型

enum MessageType {
  FOO = "FOO",
  BAR = "BAR",
  BAZ = "BAZ",
}

interface DataFoo {
  type: MessageType.FOO
  payload: string
}

interface DataBarOrBaz {
  type: MessageType.BAR | MessageType.BAZ
  name: string
  age: number
}

type Data = DataFoo | DataBarOrBaz

这是我正在开发的函数,我希望自动完成data参数

function emit<T extends MessageType>(type: T, data: ???) {}

预期的结果是这样的

emit(MessageType.FOO, {
  payload: "hello world"
  // with autocomplete for payload in DataFoo
})

emit(MessageType.BAR, {
  name: "hello"
  age: 18
  // with autocomplete for name and age in DataBarOrBaz
})

emit(MessageType.BAZ, { 
  name: "hello",
  age: 18
  // with autocomplete for name and age in DataBarOrBaz
})

然而,当我try 在TypeScrip中使用Extract实用程序类型实现预期结果时,type字段是MessageType的联合的接口不起作用

function emit<T extends MessageType>(type: T, data: Omit<Extract<Data, {type: T}>, "type">) {}
emit(MessageType.FOO, { // the type for data becomes Omit<DataFoo, "type">
  payload: "hello world" 
  // works as intended
})

emit(MessageType.BAR, { // the type for data becomes Omit<never, "type">
// expecting the type to be Omit<DataBarOrBaz, "type">
})

emit(MessageType.BAZ, { // the type for data becomes Omit<never, "type">
// expecting the type to be Omit<DataBarOrBaz, "type">
})

有没有办法使用TypeScrip v5来实现这一目标?

Typescript Playground

推荐答案

您的方法实际上是正确的,然而,问题出在Extract.更准确地说,问题出在Extract判断T是否扩展了另一个类型的方式上:

/**
 * Extract from T those types that are assignable to U
 */
type Extract<T, U> = T extends U ? T : never;

这种情况下的问题是,在这种情况下,T是与DataBarOrBaz的并集:

interface DataBarOrBaz {
  type: MessageType.BAR | MessageType.BAZ;
  name: string;
  age: number;
}

所以,当我们实际将MessageType.BAZ传递给emit时,我们得到这样的条件:

// false
type Case1 = MessageType.BAR | MessageType.BAZ extends MessageType.BAZ ? true : false

这显然是错误的,因为左边的部分没有右边的具体,这使得右边的条件是左边的subset%,因为它更具体.

因此,要解决该问题,我们应该扭转情况,以:

// true
type Case2 = MessageType.BAZ extends MessageType.BAR | MessageType.BAZ ? true : false

为了实现这一点,我们将使用mapped types来映射消息的每个版本,并使用key remapping来添加前面提到的条件.如果类型与条件匹配,那么我们将key设置为message.type,只是为了使映射的类型有效,因为我们只能在对象中使用string | number | symbol作为键,尽管我们实际上不需要键,只需要值.在值中,我们只需从消息中省略type:

type FindData<T extends MessageType> = {
  [K in Data as T extends K['type'] ? K['type'] : never]: Omit<K, 'type'>;
}

测试:

// type Result = {
//     BAR: Omit<DataBarOrBaz, "type">;
//     BAZ: Omit<DataBarOrBaz, "type">;
// }
type Result = FindData<MessageType.BAR>

这些值看起来不错,要提取它们,我们将使用here中描述的ValueOf:

type ValueOf<T> = T[keyof T];

type FindData<T extends MessageType> = ValueOf<{
  [K in Data as T extends K['type'] ? K['type'] : never]: Omit<K, 'type'>;
}>;

// type Result = {
//     name: string;
//     age: number;
// }
type Result = FindData<MessageType.BAR>;

用途:

function emit<T extends MessageType>(type: T, data: FindData<T>) {}

emit(MessageType.FOO, {
  payload: 'hello world',
});

emit(MessageType.BAR, {
  name: 'hello',
  age: 18,
});

emit(MessageType.BAZ, {
  name: 'hello',
  age: 18,
});

playground

Typescript相关问答推荐

Angular -使用Phone Directive将值粘贴到控件以格式化值时验证器不工作

如何将http上下文附加到angular中翻译模块发送的请求

如何使用axios在前端显示错误体

如何将联合文字类型限制为文字类型之一

类型脚本类型字段组合

是否使用非显式名称隔离在对象属性上声明的接口的内部类型?

TypeScrip:逐个 case Select 退出noUncheck kedIndexedAccess

在包含泛型的类型脚本中引用递归类型A<;-&B

为什么有条件渲染会导致material 自动完成中出现奇怪的动画?

用于验证字符串变量的接口成员

ANGLE独立组件布线错误:没有ActivatedRouting提供程序

如何在Typescript 中组合unions ?

是否可以判断某个类型是否已合并为内部类型?

如何以Angular 导入其他组件S配置文件

如何获取受类型脚本泛型约束的有效输入参数

是否可以在类型脚本中定义条件返回类型,而不在函数体中使用类型转换?

Typescript .根据枚举输入参数返回不同的对象类型

带有过滤键的 Typescript 映射类型

使用嵌套属性和动态执行时,Typescript 给出交集而不是并集

创建通过删除参数来转换函数的对象文字的类型