如何将对象转换为有序的可区分联合集
How to Transform a Object into an Ordered Set of Discriminated Unions
给定以下类型定义
type MailStatus = {
InvoiceSent?: Date;
ReminderSent?: {
date: Date;
recipient: string;
}
FinalReminderSent?: {
date: Date;
recipient: string;
text: string;
}
}
我想要一个类型,我可以在其中定义需要 属性 的“顺序”,并创建一个具有更多必需属性的可区分联合。
例如
type OrderedMailStatus = MagicType<MailStatus, "InvoiceSent" | "ReminderSent" | "FinalReminderSent">
//or this
type OrderedMailStatus = MagicType<MailStatus, ["InvoiceSent", "ReminderSent","FinalReminderSent"]>
应该产生以下类型
type OrderedMailStatus =
| {
kind: "InvoiceSentRequired";
InvoiceSent: Date; //InvoiceSent now required
ReminderSent?: {
date: Date;
recipient: string;
};
FinalReminderSent?: {
date: Date;
recipient: string;
text: string;
};
}
| {
kind: "ReminderSentRequired";
InvoiceSent: Date; //InvoiceSent required
ReminderSent: { //ReminderSent also required
date: Date;
recipient: string;
};
FinalReminderSent?: {
date: Date;
recipient: string;
text: string;
};
}
| {
kind: "FinalReminderSentRequired";
InvoiceSent: Date; //all
ReminderSent: { //3 properties
date: Date;
recipient: string;
};
FinalReminderSent: { //are required
date: Date;
recipient: string;
text: string;
};
}
这样我就可以完成以下作业
const s1 = {
kind: "InvoiceSentRequired",
InvoiceSent: new Date()
} //OK
const s2 = {
kind: "ReminderSentRequired",
InvoiceSent: new Date(),
ReminderSent: {
date: new Date(),
recipient: "Somebody@somewhere.com"
}
} //OK
const s3 = {
kind: "FinalReminderSentRequired",
ReminderSent: {
date: new Date(),
recipient: "Somebody@somewhere.com"
},
FinalReminderSent: {
date: new Date(),
recipient: "Somebody@somewhere.com",
text: "YOU HAVE TO PAY!"
}
} //FAILS because it is missing the property InvoiceSent
同样重要:属性的类型应自动采用它们在原始 MailStatus
中的类型。因此,即使在这个扩展示例中,您也不能做出任何假设 属性 具有哪种类型。
这个问题背后的主要思想与 Workflow
类似。一开始你有一个类型,它的属性都是可选的。随着这种类型在系统中传播,越来越多的属性成为强制性的
首先,值得创建一个具有预期键顺序的元组:
type Keys = ["InvoiceSent", "ReminderSent", "FinalReminderSent"]
现在我们需要创建一个实用程序类型,它将遍历 Keys
元组并创建预期的并集。
type Union<
Tuple extends any[],
Result extends {} = never
> =
// Obtain first element from the Tuple
(Tuple extends [infer Head, ...infer Rest]
// Check whether this element extends allowed keys
? (Head extends keyof MailStatus
// call Union recursively with Rest
// and unionize previous Result with newly created discriminated union
? Union<Rest, Result | MailStatus & Record<Head, Date> & { kind: `${Head}Required` }>
: never)
: Result)
游乐场
让我们测试一下:
type Result = Union<Keys>
// ok
const result: Result = {
kind: 'InvoiceSentRequired',
InvoiceSent: new Date()
}
// expected error
const result2: Result = {
kind: 'InvoiceSentRequired',
}
// ok
const result3: Result = {
kind: 'FinalReminderSentRequired',
FinalReminderSent: new Date
}
看起来有效
P.S。如果您对某些特定订单感兴趣,请不要依赖 keyof
运算符。参见 this issue and my question
这是我对这个问题的解决方案:
type Id<T> = {} & { [P in keyof T]: T[P] }
type PickIfNotPrimitive<T, K extends keyof T, V = T[K]> =
V extends Date | string | number | bigint | boolean
? Record<K, V>
: Pick<T, K>
type Accumulate<T, Keys extends string[], B = {}, R = never> =
Keys extends [infer Head, ...infer Tail] ?
Tail extends string[]
? Accumulate<
T,
Tail,
Required<PickIfNotPrimitive<T, Head & keyof T>> & B,
// New result
| R & Partial<PickIfNotPrimitive<T, Head & keyof T>> // Add new partial values
| Required<PickIfNotPrimitive<T, Head & keyof T>> & B & { type: `${Head & string}Required` }>
: never
:Id<R>
type X = Accumulate<MailStatus, [
"InvoiceSent",
"ReminderSent",
"FinalReminderSent"
]>
我们在R
中将结果一一累积。 B
表示已经必填的字段。
我使用 Id
只是为了修饰类型。可以删除,但没有它结果不可读。
不确定我会推荐实际使用它,但它很有趣
给定以下类型定义
type MailStatus = {
InvoiceSent?: Date;
ReminderSent?: {
date: Date;
recipient: string;
}
FinalReminderSent?: {
date: Date;
recipient: string;
text: string;
}
}
我想要一个类型,我可以在其中定义需要 属性 的“顺序”,并创建一个具有更多必需属性的可区分联合。
例如
type OrderedMailStatus = MagicType<MailStatus, "InvoiceSent" | "ReminderSent" | "FinalReminderSent">
//or this
type OrderedMailStatus = MagicType<MailStatus, ["InvoiceSent", "ReminderSent","FinalReminderSent"]>
应该产生以下类型
type OrderedMailStatus =
| {
kind: "InvoiceSentRequired";
InvoiceSent: Date; //InvoiceSent now required
ReminderSent?: {
date: Date;
recipient: string;
};
FinalReminderSent?: {
date: Date;
recipient: string;
text: string;
};
}
| {
kind: "ReminderSentRequired";
InvoiceSent: Date; //InvoiceSent required
ReminderSent: { //ReminderSent also required
date: Date;
recipient: string;
};
FinalReminderSent?: {
date: Date;
recipient: string;
text: string;
};
}
| {
kind: "FinalReminderSentRequired";
InvoiceSent: Date; //all
ReminderSent: { //3 properties
date: Date;
recipient: string;
};
FinalReminderSent: { //are required
date: Date;
recipient: string;
text: string;
};
}
这样我就可以完成以下作业
const s1 = {
kind: "InvoiceSentRequired",
InvoiceSent: new Date()
} //OK
const s2 = {
kind: "ReminderSentRequired",
InvoiceSent: new Date(),
ReminderSent: {
date: new Date(),
recipient: "Somebody@somewhere.com"
}
} //OK
const s3 = {
kind: "FinalReminderSentRequired",
ReminderSent: {
date: new Date(),
recipient: "Somebody@somewhere.com"
},
FinalReminderSent: {
date: new Date(),
recipient: "Somebody@somewhere.com",
text: "YOU HAVE TO PAY!"
}
} //FAILS because it is missing the property InvoiceSent
同样重要:属性的类型应自动采用它们在原始 MailStatus
中的类型。因此,即使在这个扩展示例中,您也不能做出任何假设 属性 具有哪种类型。
这个问题背后的主要思想与 Workflow
类似。一开始你有一个类型,它的属性都是可选的。随着这种类型在系统中传播,越来越多的属性成为强制性的
首先,值得创建一个具有预期键顺序的元组:
type Keys = ["InvoiceSent", "ReminderSent", "FinalReminderSent"]
现在我们需要创建一个实用程序类型,它将遍历 Keys
元组并创建预期的并集。
type Union<
Tuple extends any[],
Result extends {} = never
> =
// Obtain first element from the Tuple
(Tuple extends [infer Head, ...infer Rest]
// Check whether this element extends allowed keys
? (Head extends keyof MailStatus
// call Union recursively with Rest
// and unionize previous Result with newly created discriminated union
? Union<Rest, Result | MailStatus & Record<Head, Date> & { kind: `${Head}Required` }>
: never)
: Result)
游乐场
让我们测试一下:
type Result = Union<Keys>
// ok
const result: Result = {
kind: 'InvoiceSentRequired',
InvoiceSent: new Date()
}
// expected error
const result2: Result = {
kind: 'InvoiceSentRequired',
}
// ok
const result3: Result = {
kind: 'FinalReminderSentRequired',
FinalReminderSent: new Date
}
看起来有效
P.S。如果您对某些特定订单感兴趣,请不要依赖 keyof
运算符。参见 this issue and my question
这是我对这个问题的解决方案:
type Id<T> = {} & { [P in keyof T]: T[P] }
type PickIfNotPrimitive<T, K extends keyof T, V = T[K]> =
V extends Date | string | number | bigint | boolean
? Record<K, V>
: Pick<T, K>
type Accumulate<T, Keys extends string[], B = {}, R = never> =
Keys extends [infer Head, ...infer Tail] ?
Tail extends string[]
? Accumulate<
T,
Tail,
Required<PickIfNotPrimitive<T, Head & keyof T>> & B,
// New result
| R & Partial<PickIfNotPrimitive<T, Head & keyof T>> // Add new partial values
| Required<PickIfNotPrimitive<T, Head & keyof T>> & B & { type: `${Head & string}Required` }>
: never
:Id<R>
type X = Accumulate<MailStatus, [
"InvoiceSent",
"ReminderSent",
"FinalReminderSent"
]>
我们在R
中将结果一一累积。 B
表示已经必填的字段。
我使用 Id
只是为了修饰类型。可以删除,但没有它结果不可读。
不确定我会推荐实际使用它,但它很有趣