given the following type definition
type MailStatus = {
InvoiceSent?: Date;
ReminderSent?: {
date: Date;
recipient: string;
}
FinalReminderSent?: {
date: Date;
recipient: string;
text: string;
}
}
I'd like to have type where I can define the "order" in which a property is required and which creates a discriminated union which have ever more required properties.
For example
type OrderedMailStatus = MagicType<MailStatus, "InvoiceSent" | "ReminderSent" | "FinalReminderSent">
//or this
type OrderedMailStatus = MagicType<MailStatus, ["InvoiceSent", "ReminderSent","FinalReminderSent"]>
should yield the following type
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;
};
}
so that I could do the following assignments
const s1 = {
kind: "InvoiceSentRequired",
InvoiceSent: new Date()
} //OK
const s2 = {
kind: "ReminderSentRequired",
InvoiceSent: new Date(),
ReminderSent: {
date: new Date(),
recipient: "[email protected]"
}
} //OK
const s3 = {
kind: "FinalReminderSentRequired",
ReminderSent: {
date: new Date(),
recipient: "[email protected]"
},
FinalReminderSent: {
date: new Date(),
recipient: "[email protected]",
text: "YOU HAVE TO PAY!"
}
} //FAILS because it is missing the property InvoiceSent
Also important: The types of the properties should be automatically taken what ever they are in the original MailStatus. So even in this expanded example you can not make any assumptions which property has which type.
The principle idea behind this question is something along the lines of a Workflow. Where in the beginning you have a type whose properties are all optional. As this type travels across the system more and more properties become mandatory
CodePudding user response:
Here's my solution to this problem:
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"
]>
We build up in R the result one by one. B represents the fields that are already required.
I use Id just to pretty up the types. Can be removed, but the results are unreadable without it.
Not sure I would recommend actually using this, but it is fun
