Home > Enterprise >  Typescript Bidirectional Inference/Constraint
Typescript Bidirectional Inference/Constraint

Time:09-26

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.


Playground

  • Related