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.