John
John

Reputation: 17471

Typescript Union Types: Dealing with Interfaces

What's the proper way to handle a situation where you have two interfaces that are similar enough that you want to run them through the same piece of logic:

interface DescriptionItem {
    Description: string;
    Code: string;
}
interface NamedItem {
    Name: string;
    Code: string;
}

function MyLogic(i: DescriptionItem | NamedItem) {
    var desc = (<DescriptionItem>i).Description || (<NamedItem>i).Name;

    return i.Code + ' - ' + desc;
}

This works; however, my question is on improving the var desc = ... line. Is what I have above the best option? Or is there a better way to handle this situation in Typescript?

Upvotes: 14

Views: 14519

Answers (2)

Droa
Droa

Reputation: 465

I know this question is very old, but I was playing around with the same problem, as I was learning the difference between | and & when making Type Unions.

there are some options to solve this problem (that is also linter-friendly). The best way is to use a discriminator in all your interfaces (a narrow interface).

//First create a super-interface that have the discriminator
interface B
{
    kind:'b1'|'b2' //define discriminator with all the posible values as string-literals (this is where the magic is)
}

interface B1 extends B
{
    kind: 'b1' //now narrow the inherited interfaces literals down to a single 
    //after that add your interface specific fields
    data1: string;
    data: string;
}

interface B2 extends B
{
    kind:'b2' //now narrow the inherited interfaces literals down to a single
    //after that add your interface specific fields
    data2: string;
    data: string;
}

//lets initialize 1 B1 type by using the literal value of a B1 interface 'b1'
var b1: B1|B2 = {
    kind:'b1',
    data: 'Hello From Data',
    data1:'Hello From Data1'
    //typescript will not allow you to set data2 as this present a B1 interface
}
//and a B2 Type with the kind 'b2'
var b2: B1|B2 = {
    kind: 'b2',
    data: 'Hello From Data',
    //typescript will not allow you to set data1 as this present a B2 interface
    data2: 'Hello From Data2'
}

another option is to check for fields on an object using the "in"-keyword, but this can cause a lot of boilerplate code, as you have to update it every time you change your interface.

interface B1
{
    data1: string;
    data: string;
}

interface B2
{
    data2: string;
    data: string;
}

var b3: B1|B2 = {
    data: 'Hello From Data',
    data1:'Hello From Data1',
    data2:'Hello From Data2'
}

console.log(b3.data); //this field is common in both interfaces and does not need a check

if('data1' in b3) //check if 'data1' is on object
{
    console.log(b3.data1);
}
if('data2' in b3){
    console.log(b3.data2); //check if 'data2' is on object
}

Upvotes: 8

Nathan Friend
Nathan Friend

Reputation: 12804

TypeScript interfaces only exist at compile-time, so there isn't much you can do to test for interface types at run-time. The code you specified in your question makes sense and is probably your best option.

However, if you have the flexibility to change your interfaces to classes, you can use TypeScript's type guards to do more elegant type checking:

class DescriptionItem {
    Description: string;
    Code: string;
}
class NamedItem {
    Name: string;
    Code: string;
}

function MyLogic(i: DescriptionItem | NamedItem) {
    let desc: string;
    if (i instanceof DescriptionItem) {
        desc = i.Description;
    } else {
        desc = i.Name;
    }

    return i.Code + ' - ' + desc;
}

Upvotes: 14

Related Questions