typescript - Type to check if multiple Partial are a complete Interface - Stack Overflow

admin2025-04-15  0

I would like to create a type to check if all attributes of an interface are given. The attributes can be set over multiple objects.

I know, that by using conditional recursive types a list of objects can be merged. But how is it possible to compare the result of merging with the given interface?

interface Box {
  height: number,
  width: number,
  length: number,
  weight: number
}

type Check<T, I> = ???

function createBox<T>(...args: Check<T, Box>):Box {
  ...
}

Typescript should yell that weight is missing, but is required in Box.

createBox({height: 11, width: 15}, {length: 24});

Arguments can overwrite attributes from previous arguments.

createBox({height: 11, width: 15}, {length: 24, width: 3});

Please share some of your thoughs how that works for better understanding.

I would like to create a type to check if all attributes of an interface are given. The attributes can be set over multiple objects.

I know, that by using conditional recursive types a list of objects can be merged. But how is it possible to compare the result of merging with the given interface?

interface Box {
  height: number,
  width: number,
  length: number,
  weight: number
}

type Check<T, I> = ???

function createBox<T>(...args: Check<T, Box>):Box {
  ...
}

Typescript should yell that weight is missing, but is required in Box.

createBox({height: 11, width: 15}, {length: 24});

Arguments can overwrite attributes from previous arguments.

createBox({height: 11, width: 15}, {length: 24, width: 3});

Please share some of your thoughs how that works for better understanding.

Share Improve this question edited Feb 5 at 11:20 Nico Richter asked Feb 4 at 12:33 Nico RichterNico Richter 3791 silver badge10 bronze badges 8
  • Does this approach meet your needs? Please check thoroughly before responding. If it meets your needs then I can write an answer or find a suitable duplicate. If not, please edit to demonstrate use cases unsatisfied by my suggestion. – jcalz Commented Feb 4 at 13:02
  • yes, that works really nice. But I am wondering, why typescript suggests attributes only at the last argument? It would also be nice if all attrubutes will be suggested in every argument because they can be overwritten. I will add this to my question. – Nico Richter Commented Feb 5 at 8:01
  • "It would also be nice if all attrubutes will be suggested in every argument" You can't make TypeScript show an error for every argument. It picks the first one it has a problem with and complains about it. You can choose the first one or the last one. I thought the last one is more intuitive because that's when you stop giving it things but more are needed. I'm not sure why overwriting matters, but this approach should give you the error you expect. What shall we do from here? Shall I write up an answer or is something missing? – jcalz Commented Feb 5 at 14:49
  • With suggestion I mean Intellisense on the object in a argument while typing or ctrl+space. In other words: Every argument can be Partial<Box>. Showing error on last argument is best way. Overwriting comes in place, when a Box is the first argument to create another Box with similar attributes. – Nico Richter Commented Feb 5 at 17:07
  • Okay, and does this approach not do what you want? I'm trying to understand what remains for me to do other than post an answer. What, if anything, is missing from this? – jcalz Commented Feb 5 at 17:11
 |  Show 3 more comments

1 Answer 1

Reset to default 1

Essentially you want to check Check<T extends readonly object[], U extends object> to make sure that when you intersect all the elements of T together, you get something assignable to U. That's because when you Object.assign() the elements of an array of type T together you get a value of that intersection type, more or less (this is not strictly true if a property of one type get overwritten by a property of a different type, but I'm not going to worry about this here; without a true spread type operator as requested in microsoft/TypeScript#10727, intersection is a close enough approximation for our purposes).

If that check passes then Check<T, U> should just be T. If it fails, then Check<T, U> should be something that looks very much like T except where, say, the last element is modified to whatever would be necessary for the check to pass. This way you'll get an error that tells you about missing properties in your last argument.

So, that's the overall approach. Here are the details:

type TupleToIntersection<T extends readonly any[]> =
   { [I in keyof T]: (x: T[I]) => void } extends { [k: number]: (x: infer I) => void } ? I : never

type AugmentLastElement<T extends readonly any[], U> =
   T extends readonly [...infer I, any] ? [...I, Partial<U> & 
     { [K in keyof U as K extends keyof TupleToIntersection<I> ? never : K]: U[K] }] :
   [U];

type Check<T extends readonly any[], U> =
   T extends (TupleToIntersection<T> extends U ? unknown : never) ? T :
   AugmentLastElement<T, U>;

The TupleToIntersection<T> type just computes the intersection as described in Is there any way to get intersection type of generic rest parameters?. So if T is [{a: string, b: number}, {c: boolean}], then TupleToIntersection<T> is {a: string, b: number} & {c: boolean}.

Then AugmentLastElement<T, U> checks if T has a last element. If so, it replaces that element with effectively Omit<U, keyof TupleToIntersection<I>> where I is the initial part of T without the last element. So if T is [{a: string, b: number}, {c: boolean}] and U is {a: string, b: number, c: boolean, d: Date}, I is [{a: string, b: number}] (the last element of T is gone) and keyof TupleToIntersection<I> is "a" | "b", and Omit<U, keyof TupleToIntersection<I> is effectively {c: boolean, D: Date}. I'm not using the Omit<T, K> utility type directly, but rather I'm using a key remapped type with as to filter out the keys (mapping a key to never suppresses it). And I'm also intersecting with Partial<U> so that the error message doesn't complain about "excess" properties if your last element repeats properties from earlier. Oh and finally if T is an empty array type, then AugmentLastElement<T, U> is just [U], since that means you passed in no arguments, so the closest correct argument list would be a single argument of type U.

And finally Check<T, U> checks that TupleToIntersection<T> extends U. If it does, Check<T, U> evaluates to T (so the check passes, since we are comparing T to Check<T, U>). If it does not, then it evaluates to AugmentLastElement<T, U>, so you'll get a reasonable error about what you did wrong.


Let's test it out:

interface Box {
   height: number,
   width: number,
   length: number,
   weight: number
}
function createBox<T extends Partial<Box>[]>(...args: Check<T, Box>): Box {
   return Object.assign({}, ...args);
}

createBox(); // error, Expected 1 arguments, but got 0.
createBox({ height: 11, width: 15 }, { length: 24 }); // error!
// Property 'weight' is missing -->  ~~~~~~~~~~~~~~
createBox({ height: 11, width: 15 }, { length: 24, weight: 100 }); // okay
createBox({ height: 1 }, { weight: 2 }, { length: 3 }, { width: 4 }); // okay
createBox({ height: 11, width: 15 }, { length: 24, width: 3 }); // error!
// Property 'weight' is missing -->  ~~~~~~~~~~~~~~~~~~~~~~~~

Looks good. It passes and fails where it's supposed to, and the error messages mention what needs to be done to fix it.


That's as close as I can get to the desired behavior.

Note that by making createBox generic but conditional in the type T of args, it is easy for TypeScript to fail to infer T properly. If T cannot be inferred by the way Check<T, Box> is written, it falls back to the constraint of Partial<Box>[], which would not be useful for us, as that loses track of which particular properties were actually passed.

Such inference is quite fragile. If you want to have IntelliSense prompt you for all the properties of Box in each argument, you'd need to have each argument's type be of type, say, Partial<Box>, as well as the actual passed-in type. But when we do that, it breaks the inference. I have to leave it alone here, unless you want to refactor your code away from a single function call. You might be able to write it like createBox({ height: 11, width: 15 }).and({ length: 24, weight: 100 }).build() where build() would fail unless you had all the properties, but this is out of scope for the question as asked. So again, I'm leaving it like this.

Playground link to code

转载请注明原文地址:http://www.anycun.com/QandA/1744719458a86673.html