1

I have a set of interfaces, and a Generic interface. This generic interface, can potentially extend any interface in the set of interfaces, but if it does, I want it to require all the values in the extended interface

interface one {
   b: boolean,
   c: number
}

interface two {
   d: string[]
   f: boolean[]
}

function F<K>(p : K) =>.....

I would actually have something like K extends one & two

But this should be an error:

const a = {b: true, c:2, d:["as"]}
F(a)

http://www.typescriptlang.org/play/#code/JYOwLgpgTgZghgYwgAgPYhQbwLAChkEBGAXMoaqgDYRwgA0eByCpIArgLaHR4C+eeUJFiIUYAO6pkOfAQAmpAM5gooAOYBtALqMCMUuSo0Q2vgNww2IBGGDpkAMQA8AaWQQAHpBBzFaDMgAPsgSUgB8ABQADsikLgCUMgQI6IpGAHSUqGrR8Wa4eCkgyshwyAC80iQhUGwQdMzEAEwNChoARHCK7Vr8uA4RcPFAA

1 Answers1

1

I'm renaming your types to make them more conventional:

interface One {
  b: boolean;
  c: number;
}

interface Two {
  d: string[];
  f: boolean[];
}

One of the issues here is that types in TypeScript are extendible and not exact. So the value {b: true, c:2, d:["as"]} is a perfectly valid instance of One | Two, since it is assignable to One. Normally you'd get excess property checking to warn you about d, but since d is part of Two, the compiler won't see that as an excess property.

One idea would be to use a mapped type to say "something with none of the properties of T", like this:

type NoneOf<T> = { [K in keyof T]?: never };

So then NoneOf<One> would be {b?: never, c?: never} and NoneOf<Two> would be {d?: never, f?: never}. Then the type you're looking for would be

type DesiredType = (One | NoneOf<One>) & (Two | NoneOf<Two>);

And you could see it work:

const a: DesiredType = {b: true, c:2, d:["as"]};  // error
const okay: DesiredType = {b: true, c: 2}; // okay

But you said you wanted to have N interfaces, like say

interface Three {
  g: number[];
  h: string;
}

And you want something programmatic, I guess, that turns (One | Two | Three | Four | ...) into (One | NoneOf<One>) & (Two | NoneOf<Two>) & (Three | NoneOf<Three>) & (Four | NoneOf<Four>) & .... This is possible, use a variant of a solution to turn unions into intersections:

type UnionToAllOrNone<U> = (U extends any
  ? (k: U | NoneOf<U>) => void
  : never) extends ((k: infer I) => void)
  ? I
  : never;

And then let's see it work with One | Two | Three:

type I = UnionToAllOrNone<One | Two | Three>;

function f(i: I) {}

f({}); // none of them
f({ b: true, c: 1 }); // just One
f({ d: [""], f: [true] }); // just Two
f({ g: [1], h: "" }); // just Three
f({ b: true, c: 1, d: [""], f: [true] }); // One and Two
f({ b: true, c: 1, d: [""], f: [true], g: [1], h: "" }); // One, Two, Three

Those are all accepted, while the following is rejected:

f({ b: true, c: 1, d: [""] }); // error! 
/* 
 Argument of type '{ b: true; c: number; d: string[]; }' 
 is not assignable to parameter of type '(One & Two & Three) | 
 (One & Two & NoneOf<Three>) | (One & NoneOf<Two> & Three) | 
 (One & NoneOf<Two> & NoneOf<Three>) | (NoneOf<One> & Two & Three) | 
 (NoneOf<One> & Two & NoneOf<...>) | 
 (NoneOf<...> & ... 1 more ... & Three) | 
 (NoneOf<...> & ... 1 more ... & NoneOf<...>) 
*/

The error message might be a bit cryptic, since it basically is a union of 2-to-the-power-of-N intersections, but hopefully it's good enough.

Okay, hope that helps; good luck!

Link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Thanks a lot for your help! Really appreciated it. Would you mind explaining a little bit more why `& (Two | NoneOf)` this works – Gabriel Gimenez Jul 24 '19 at 10:26
  • Are you aware of how [union types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#union-types) and [intersection types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#intersection-types) work? `A | B` means either `A` or `B` (and this is an *inclusive* "or", since something which is both an `A` and a `B` is a valid `A | B`), and `A & B` means both an `A` and a `B`. Since types are not exact, `One & Two` means it has the properties from `One` and the properties from `Two`. – jcalz Jul 24 '19 at 13:13
  • So, something assignable to `(One | NoneOf) & (Two | NoneOf)` must either be a `One` or a `NoneOf`, and it also must be either a `Two` or a `NoneOf`. That is the type you want to describe... it either has all or none of the properties of `One`, and all or none of the properties of `Two`. Does that make sense to you? – jcalz Jul 24 '19 at 13:14