Although this question has been around for a while, I recently delved into the same issue while learning about the distinctions between | and & in creating Type Unions.
There are various ways to address this dilemma (while also keeping it linter-friendly). The most effective approach is to employ a discriminator in all your interfaces (referred to as a narrow interface).
//Begin by crafting a super-interface with the discriminator
interface B
{
kind: 'b1' | 'b2' //define the discriminator with all possible values as string literals (this is where the magic lies)
}
interface B1 extends B
{
kind: 'b1' //narrow down the literals inherited by the interfaces to a single value
//then include your interface-specific fields
data1: string;
data: string;
}
interface B2 extends B
{
kind: 'b2' //narrow down the literals inherited by the interfaces to a single value
//then include your interface-specific fields
data2: string;
data: string;
}
//Create a B1 type instance using the literal value 'b1' from the B1 interface
var b1: B1 | B2 = {
kind: 'b1',
data: 'Hello From Data',
data1: 'Hello From Data1'
//typescript will prevent setting data2 as this represents a B1 interface
}
//And a B2 Type with the kind 'b2'
var b2: B1 | B2 = {
kind: 'b2',
data: 'Hello From Data',
//typescript will prevent setting data1 as this represents a B2 interface
data2: 'Hello From Data2'
}
Another option is to verify fields on an object using the "in"-keyword, but this may lead to extensive boilerplate code, requiring updates every time the interface changes.
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 require a check
if ('data1' in b3) //check if 'data1' exists in the object
{
console.log(b3.data1);
}
if ('data2' in b3) {
console.log(b3.data2); //check if 'data2' exists in the object
}