Home > Software engineering >  TypeScript: Key in interface from Enum, Optional but at least 1
TypeScript: Key in interface from Enum, Optional but at least 1

Time:10-01

I would like to have an interface for an object which has a userId or a customerId. Either one, but one.

The key is supposed to be defined by an enum (so naming is always the same).

I used this way, from another example:

export enum EId {
  userId = 'userId',
  customerId = 'customerId',
}

export type IIdParam = {
  [key in EId]: string;
};

export interface IIdParamString extends IIdParam {
  paramString: string;
}

In the end I would like to have this object:

  const params: IIdParamString = {
    userId: String('1234'),
    paramString: 'limit',
  };

But I get an error: When [key in EId] is not optional, it complains it needs userId and customerId and if not optional, neither ...

Maybe that is not the right approach for what I want. I guess the value in the enum is actually not needed for the key, but I can not better abstract it from the example.

CodePudding user response:

Consider this example:


export enum EId {
    userId = 'userId',
    customerId = 'customerId',
}

type AtLeastOne<Obj, Keys = keyof Obj> = Keys extends keyof Obj ? Record<Keys, string> : never

// credits goes to https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753
type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> =
    T extends any
    ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;

type StrictUnion<T> = StrictUnionHelper<T, T>

export type IIdParamString =
    & StrictUnion<AtLeastOne<typeof EId>>
    & {
        paramString: string;
    }

/**
 * Ok
 */
const params: IIdParamString = {
    userId: '1234',
    paramString: 'limit',
};

/**
 * Expected errors
 */
const params2: IIdParamString = {
    paramString: 'limit',
};
const params3: IIdParamString = {
    userId: '1234',
    customerId: 'sdf',
    paramString: 'limit',
};

AtLeastOne - you can find full explanation here

StrictUnion - you can find full explanation here. This utility makes similar trick with never which you did.

Is my approach in general conventional to my problem, to make sure it is either this or that

Yes, your approach is ok. But I recommend you to use discriminated unions. They are also known as tagged unions. For instance see F# discriminated unions. As you might have noticed, each union has own tag/flag/marker. It helps compiler to distinguish them.

Could it be an interface ? Unfortunately - no. It could be only type, because interface can extends only statically known type. See example:

// An interface can only extend an object type or intersection of object types with statically known members.
export interface IIdParamString extends StrictUnion<AtLeastOne<typeof EId>> { // error
    paramString: string;
}

Regarding your last question, I'm not sure if I understand it. You can update your question or (the better) option to ask separate.

  • Related