1

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.

Ehrlich_Bachman
  • 772
  • 1
  • 10
  • 23
  • I am not able to reproduce the error. Please read [Minimal, Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example). Please share the link of https://www.typescriptlang.org/play of the example. – Shivam Singla Sep 30 '21 at 12:39
  • I can not write it with less code. If you paste this into a .ts-file, you should get an error like: Property 'customerId' is missing in type '{ userId: string; paramString: string; }' but required in type 'IIdParamString' – Ehrlich_Bachman Sep 30 '21 at 12:45
  • I see, you edited your question. It will result in the error now, indeed. You can use discriminated unions for this. I will post the solution. – Shivam Singla Sep 30 '21 at 12:52
  • Please let me know if this is what you are looking for: https://stackoverflow.com/questions/69246630/how-to-express-at-least-one-of-the-exisiting-properties-and-no-additional-prope – captain-yossarian from Ukraine Sep 30 '21 at 12:59

1 Answers1

3

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.

  • Yes. That is exatly what I want and works like a charm. Thank you so much for understanding and solving. I am still so new to TypeScript, that my example, is the most advanced code I can write so far.I know slighly about generics ... I call my self very happy, if you could even add some explanation. Simple question at first: Why is IIdParamString not working as an interface but type? – Ehrlich_Bachman Sep 30 '21 at 13:25
  • And another question: Is my approach in general conventional to my problem, to make sure it is either this or that? Thanks again so much for this amaaaazing help. – Ehrlich_Bachman Sep 30 '21 at 13:32
  • 1
    ... could it be an interface. My plan was, that I extend from that intereface some more, which can be implemented into classes ... – Ehrlich_Bachman Sep 30 '21 at 13:51
  • `type` is more flexible than `interface` in this case. Please give me one hour for the explanation – captain-yossarian from Ukraine Sep 30 '21 at 13:58
  • 1
    Sorry, of course. No rush meant. Just got insanly curious and questions arising. – Ehrlich_Bachman Sep 30 '21 at 14:00
  • Just another thought, again no rush intended: Instead of extending a Class from an interface, I would use in the cass (constrcutor) the type IIdParamString to check if I get, what I want? – Ehrlich_Bachman Sep 30 '21 at 14:34
  • I made an update, let me know if you have further questions. Btw, I have typescript blog https://catchts.com/ , you can find a lot of interesting examples there – captain-yossarian from Ukraine Sep 30 '21 at 15:42
  • 1
    Thanks you so much, that is great helps a lot. As well to broaden my TypeScript hoirzion. I am going through it soon. – Ehrlich_Bachman Oct 01 '21 at 09:35