usr48
usr48

Reputation: 111

Create an interface in typescript in which only one of two properties are allowed?

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 ?

Upvotes: 2

Views: 1874

Answers (3)

Krishna Jangid
Krishna Jangid

Reputation: 5410

This following code will optional 2 properties from an interface

type Optional<T, K extends keyof T, K2 extends keyof T> = Partial<Pick<T, K | K2>>  & Omit<T, K | K2>

interface FooDatabase {
  _id: number;
  foo: string;
  bar: string;
  fname:string;
}

type Foo = Optional<FooDatabase, '_id', 'foo'>

const data: Foo = {
//   _id:1,
  bar:"dd",
  fname:"username", 
//   foo:"sa"
}

here in Optional<FooDatabase, '_id', 'foo'> we are passing 2 keys _id and foo so this both key will be optional.

Upvotes: 0

jcalz
jcalz

Reputation: 330276

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

Upvotes: 2

Maciej Sikora
Maciej Sikora

Reputation: 20162

// 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.

Upvotes: 0

Related Questions