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
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 😅