1

Let say I have an interface

interface ICart {
    property1?: string,
    propert2?: string,
    someOtherProperty: string
}

How can I enforce that only one of property1 and property2 are allowed but one of them needs to be there ?

usr48
  • 101
  • 2
  • 6
  • Make it a union of two types: https://www.typescriptlang.org/docs/handbook/advanced-types.html#union-types – jonrsharpe Nov 09 '19 at 18:32
  • `{property1: string, property2?: never, someOtherProperty: string} | {property1?: never, property2: string, someOtherProperty: string}` should work – jcalz Nov 09 '19 at 18:32
  • @jcalz is there a concise way to do this in the case the interface is large – usr48 Nov 09 '19 at 18:35
  • Like [this](https://stackoverflow.com/a/57576688/2887218)? – jcalz Nov 09 '19 at 18:52
  • @jcalz thanks, I think it may be something I can use but I am still a beginner at typescript so it may require some more reading to understand it. – usr48 Nov 09 '19 at 18:58
  • Or [this](https://www.typescriptlang.org/play/#code/C4TwDgpgBA8gdhA0hEAxATgewLYB4AqANFALJQC8UA3gL7GJQQAewEcAJgM5QDWKmAMyj4KvfkPwA+CgFgAUFEVQGzVh24BDOCCgB+KAAoyAMigAFAJYBjHrgBKEAI4BXC+gjsCk+tNNmN6MAWGgA29hBWmOieAKJMViHO7BC4fCCCwj7ECABuEOiSkgCUjCxsXFAWcAL5sHryStRQANpmlXBi6UIwALoAXLCtPVA0UAO5teMQeegA3PKgkFAAkgDCAcCi8EgoGDi4VFBgWJCBIACMA5zA6FUA5sTHmKegAExXN-cjxIecOBAwYAAC3yZhO+VAH1ucDuI0ksyAA) maybe – jcalz Nov 09 '19 at 18:58
  • @jcalz wow thanks, will this snippet work if I just changed ```{ someOtherProperty: string }``` with ```{someOtherProperty1:string, someOtherProperty2: string //and so on}``` – usr48 Nov 09 '19 at 19:02
  • I don't know because I don't know what your use case is. In the linked code, `OneKeyFrom` will require all properties from `U` and exactly one property from `T`. So if you have more properties to require, add them to `U`. If you have more properties to switch between, add them to `T`. I will write this up as an answer when I get the time (unless someone gets there first). Good luck! – jcalz Nov 09 '19 at 19:06

2 Answers2

1

If you want to allow exactly one property from a list, you need a union of object types where each one allows a particular property and disallows all others. TypeScript doesn't exactly allow you to disallow a particular property, but you can do something close: make it an optional property whose value type is never. In practice this will allow a property of type undefined, but there's not a lot of difference between undefined properties and missing properties (and the difference isn't captured well in TypeScript with normal compiler options see ms/TS#13195).

So, for your example above, the type you want looks like:

type ICartManual = {
    property1: string;
    property2?: undefined;
    someOtherProperty: string;
} | {
    property1?: undefined;
    property2: string;
    someOtherProperty: string;
}

And you can verify that it behaves as you desire:

const i1: ICartManual = {
    property1: "prop1",
    someOtherProperty: "other"
}

const i2: ICartManual = {
    property2: "prop2",
    someOtherProperty: "other"
}

const iBoth: ICartManual = { // error!
//    ~~~~~ <-- property1 is incompatible with undefined
    property1: "prop1",
    property2: "prop2",
    someOtherProperty: "other"
}

const iNeither: ICartManual = { // error!
//    ~~~~~~~~ <-- property2 is missing
    someOtherProperty: "other"
}

If you have a large interface and want to take two object types T and U and make a new one which requires exactly one property from T and all properties from U, you can define it like this:

type OneKeyFrom<T, M = {}, K extends keyof T = keyof T> = K extends any ?
    (M & Pick<Required<T>, K> & Partial<Record<Exclude<keyof T, K>, never>>) extends infer O ?
    { [P in keyof O]: O[P] } : never : never;

That uses a bunch of mapped and conditional types to build the union you want. I could explain how it works but it would take a lot of words. I've done similar before; look here for a more in-depth description of a similar type.

Anyway, we can define ICart like this now:

type ICart = OneKeyFrom<{ property1: string, property2: string }, { someOtherProperty: string }>;

and you can verify (via IntelliSense, for example) that it is the same as the manually-written type (except for the order the properties are written in, which doesn't change the type):

/* type ICart = {
    property1: string;
    property2?: undefined;
    someOtherProperty: string;
} | {
    property2: string;
    property1?: undefined;
    someOtherProperty: string;
} */

Link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
0
// utility type which blocks two properties of the object coexisting 
type NeverTogether<A extends object, Key1 extends keyof A, Key2 extends keyof A extends Key1 ? never : keyof A> = 
  Omit<A, Key1 | Key2> & (({
    [k in Key1]: A[Key1]
  } & {[k in Key2]?: never}) | ({
    [k in Key1]?: never
  } & {[k in Key2]: A[Key2]}))

interface ICart {
    property1: string,
    property2: string,
    someOtherProperty: string
}

type IC = NeverTogether<ICart, 'property1', 'property2'>;

// error never together
const a: IC = {
  property1: '1',
  property2: '2',
  someOtherProperty: '2'
}

// error one needs to be there
const b: IC = {
  someOtherProperty: '2'
}

// correct
const c: IC = {
  property2: '2',
  someOtherProperty: '2'
}

// correct
const d: IC = {
  property1: '1',
  someOtherProperty: '2'
}

The issue which NeverTogether type has is composing it in order to have such rule for more keys. So works nice for two dependent fields, but cannot have it working for more. But maybe this will help you. For me it was nice puzzle to solve.

Maciej Sikora
  • 19,374
  • 4
  • 49
  • 50