Sorry about the vague title, I spend a decent 30 minutes trying to think of how to cleanly summarise this and I can't think of anything better.
So, after removing all the business logic, the situation I would like to achieve looks something like this:
const Func: <
T extends (PartA & PartB & PartC),
PartA extends Partial<T> = {},
PartB extends Partial<T> = {},
PartC extends Partial<T> = {}
> (parts: { partA: PartA, partB: PartB, partC: PartC }) => T;
Essentially the aim here is to have T be optional, such that if Func() is called without T, then T will be inferred from the structures of PartA, PartB, and PartC, and if T is passed (as Func<T>()), then it will be used to constrain the types of PartA, PartB, and PartC.
But of course I can't do it as I set it up in the example above, because that causes T to be a circular constraint, and I can't think of any way around that. But I also know that there are various tools in TS like infer and such that might be useful if I knew how to implement them properly. So, does anyone know of a way in which I can manipulate the way these generics are defined in order to make this work?
CodePudding user response:
You could change the definiton like this:
const func: <
T extends (A & B & C),
A = unknown,
B = unknown,
C = unknown
> (parts: {
partA: A & Partial<T>,
partB: B & Partial<T>,
partC: C & Partial<T>
}) => T = null!;
The three generic types A, B and C get a default value of unknown to make them optional. For the part properties, we can use an intersection of N & Partial<T>. This makes it possible to infer the generic type for each part but also to make them comply to the Partial<T> type.
Let's check some tests.
const res1 = func({ partA: { x: "" }, partB: { y: 0 }, partC: { z: new Date() } })
// ^? const res1: { x: string; } & { y: number; } & { z: Date; }
The inference of T based on the input object seems to work as expected.
const res2 = func<{ x: string, y: number, z: Date }>({
partA: { x: "0" },
partB: { y: 0 },
partC: { z: new Date() }
})
const res3 = func<{ x: string, y: number, z: Date }>({
partA: { x: "0" },
partB: { y: "0" },
// ^ Error: we violate Partial<T>
partC: { z: new Date() } })
When we explitly provide a type for T, the constraint Partial<T> is enforced for each part.
The only drawback to keep in mind is the following scenario.
const res4 = func<{ x: string, y: number, z: Date }>({
partA: { x: "0" },
partB: { y: "0" },
partC: { z: "0" }
})
Each part has to be a Partial<T> but they don't have to add up to be a whole T. They can all be empty objects and TypeScript would be satisfied. This only is an issue when an explicit type for T is specified though.
