Kamil Janowski
Kamil Janowski

Reputation: 2025

How to configure a conditional type based on value of a property?

I have a number of Item objects and a method that processes them. The issue is that some Items could potentially have certain properties that others don't.

type ItemType = 'ApiGateway' | 'ApiGatewayEndpoint' | 'ApiGatewayMethod';

export default interface Item {
    ref?: string;
    position: Position;
    type: ItemType;
    children?: Item[];
}

function process(item: Item) {...}

Now, let's say I want to create a separate interface ApiGatewayMethodItem (that extends Item) that has an additional string property called 'method' representing whether it's a GET or POST or something else. Is there a way I can type it in such a way that as soon as I type process({type: 'ApiGatewayMethod'}) tsc will start complaining about the missing property method ? I understand that TS has a pretty good support for the "conditional types", but I haven't used them before and I'm having a hard time wrapping my head around them...

So let's say I have the interface

interface ApiGatewayMethodItem extends Omit<Item, 'type'> {
    type: 'ApiGatewayMethod';
    method: string;
}

now when I call the process function, I need the compiler to complain that the method property is missing when I don't specify it, but specify the type ApiGatewayMethod

Upvotes: 1

Views: 260

Answers (3)

Matthias Gwiozda
Matthias Gwiozda

Reputation: 595

You could define your item type with only the 2 ItemTypes that doesn't have the property 'method'. Then add the property 'method' only to the type 'ApiGatewayMethodItem'

type ItemType = 'ApiGateway' | 'ApiGatewayEndpoint';

export default interface DefaultItem {
    ref?: string;
    position: Position;
    type: ItemType;
    children?: DefaultItem[];
}

interface ApiGatewayMethodItem extends Omit<DefaultItem, 'type'> {
    type: 'ApiGatewayMethod';
    method: string;
}

type Item = DefaultItem | ApiGatewayMethodItem;

function process(item: Item) {

}

process ({
    type: 'ApiGatewayMethod',
    position: null
})

Upvotes: 2

Christian Held
Christian Held

Reputation: 2828

You could solve this by using discrimated unions. This way the compiler can figure out which members are available from the type field.

type ItemTypeWithoutMethod = 'ApiGateway' | 'ApiGatewayEndpoint' | 'ApiGatewayMethod';
type ItemTypeWithMethod = "ApiGatewayMethodItem";

interface ItemBase {
    ref?: string;
    position: Position;
    children?: Item[];
}

interface ItemWithoutMethod extends ItemBase {
  type: ItemTypeWithoutMethod
}

interface ItemWithMethod extends ItemBase {
    type: ItemTypeWithMethod;
    method: string;
}

export type Item = ItemWithMethod | ItemWithoutMethod;

function doSomethingWithItem(item: Item) {
  if (item.type == "ApiGatewayMethodItem") {
    console.log(item.method);
  }
}

Playground link

Upvotes: 0

OliverRadini
OliverRadini

Reputation: 6476

You can make Item a union type, and define an interface which holds the properties which are common to all items:

export default interface CommonItemProperties {
    ref?: string;
    position: Position;
    children?: Item[];
}

interface ApiGatewayMethodItem extends CommonItemProperties {
    type: 'ApiGatewayMethod';
    method: 'GET' | 'POST';
}

interface ApiGatewayEndpointItem extends CommonItemProperties {
  type: 'ApiGatewayEndpoint';
  path: string;
}

type Item = ApiGatewayEndpointItem | ApiGatewayMethodItem;

function process(item: Item) {
  switch (item.type) {
    case ('ApiGatewayMethod'):
      return item.method; // this is not a type error - typescript 'knows' the item is a ApiGatewayMethodItem
    case ('ApiGatewayEndpoint'):
      return item.path;   // this is not a type error - typescript 'knows' the item is a ApiGatewayEndpointItem
  }
}

The switch statement is just an example of how typescript can determine the type of item you've passed in based on a check against type.

Upvotes: 1

Related Questions