distante
distante

Reputation: 7025

How to declare an optional generic type in an interface?

I have this interface:

export interface AlertInteraction<T> {
  id: AlertId;
  data: T;
  buttonId: AlertButtonId;
}

But there are times where data is not needed. I know I can declare it as data: T but I would like to know if I could something like this:

export interface AlertInteraction<T> {
  id: AlertId;
  data: T;
  buttonId: AlertButtonId;
}

export interface AlertInteraction {
  id: AlertId;
  buttonId: AlertButtonId;
}

So, if I give T then I assume I want to access the data, if no then assume it does not exist. Is it possible?

Upvotes: 12

Views: 8657

Answers (3)

Maciej Sikora
Maciej Sikora

Reputation: 20162

In order to achieve that we need to use conditional types.

type AlertId = string; // just example type alias
type AlertButtonId = string; // just example type alias
type AlertInteraction<T = undefined> = {
  id: AlertId;
  buttonId: AlertButtonId;
} & (T extends undefined ? {} : {
  data: T;
})


// example usage
// no need for data (T is undefined)
const a: AlertInteraction = {
  id: '1',
  buttonId: '1'
}
// data prop is required 
const b: AlertInteraction<number> = {
  id: '1',
  data: 1,
  buttonId: '1'
}

Most important is this part:

& (T extends undefined ? {} : {
  data: T;
})

We join conditionally or type {} if T is assignable to undefined, or we join {data: T} for other type. In result if we don't set the generic, it is by default undefined and our type constructor computes into:

// For T == undefined
{
  id: AlertId;
  buttonId: AlertButtonId;
} & {}
// what is equal to just:
{
  id: AlertId;
  buttonId: AlertButtonId;
}

For a case where argument(generic) is provided as something different then undefined, then the final type has a form:

// For T != undefined
{
  id: AlertId;
  buttonId: AlertButtonId;
} & { data: T}
// which is equal to
{
  id: AlertId;
  buttonId: AlertButtonId;
  data: T;
}

Where T is type given by argument of type constructor. To also clear the information - Type constructor = any type definition with an generic argument.

Upvotes: 14

Ali Habibzadeh
Ali Habibzadeh

Reputation: 11577

Create a second wrapping type that when it is provided a type it doesn't omit data but when there is no type provided, then data field is omitted.

As you stated, making data optional won't be the right thing as you need the field to not exist at all when you don't need it.

export interface IInterationData<T> {
  id: AlertId;
  data: T;
  buttonId: AlertButtonId;
}

type IAlertInteraction<T = undefined> = T extends undefined
  ? Omit<IInterationData<any>, "data">
  : IInterationData<T>;

export const a: IAlertInteraction = {
  id: 1,
  buttonId: ""
};

export const b: IAlertInteraction<number> = {
  id: 1,
  data: 2,
  buttonId: ""
};

Upvotes: 2

Abdelrhman ElSayed
Abdelrhman ElSayed

Reputation: 439

You can try something like this

export interface AlertInteraction<T = any> {
  id: AlertId;
  data?: T;
  buttonId: AlertButtonId;
}

// those are valid:

const x: AlertInteraction = {id: 'AlertId', buttonId: 'AlertButtonId'};

// Accept the data property as number only.
const x: AlertInteraction<number> = {id: 'AlertId', buttonId: 'AlertButtonId', data: 123};

Or you can make them 2 interfaces , one without data property and generic param and the second extend the first one and has the data property and generic param

something like this


export interface AlertInteractionBase {
  id: string;
  buttonId: string;
}

export interface AlertInteraction<T> extends AlertInteractionBase {
  data: T;
}

Upvotes: -1

Related Questions