This line of code AddressBookingStop|ShowBookingStop
means that TS, by the default, will allow you to use only those properties which are common for both interfaces. THis is safe behavior.

COnsider this example:
type A = {
tag: string,
a: 'a'
}
type B = {
tag: string,
b: 'b'
}
type C = keyof (A | B) // tag
Nevertheless, there is always a workaround in TypeScript type system :D
YOu can add optional property with never
type:
type A = {
tag: string,
a: 'a'
b?: never
}
type B = {
tag: string,
b: 'b',
a?: never
}
//"tag" | "a" | "b"
type C = keyof (A | B)
declare var b: B
if (b.a) {
const x = b.a // never
}
You might have noticed that in last example all properties are allowed in a type scope. Don't worry, using disallowed prop b.a
is impossible because it has never
type.
Let's go back to our example:
interface AddressBookingStop {
street: string
placeName: string
city: string
province: string
postalCode: string
}
interface ShowBookingStop {
id: number;
city: string;
postal_code: string;
place_name: string;
province: string;
street: string;
segment_id: number;
order: number;
}
type UnionKeys<T> = T extends T ? keyof T : never;
// credits goes to https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753
type StrictUnionHelper<T, TAll> =
T extends any
? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>
function formatAddress(stop: StrictUnion<AddressBookingStop | ShowBookingStop>): string {
let address = [stop.street || stop.placeName || stop.place_name, stop.city, stop.province].filter(s => s).join(', ');
const postalCode = stop.postalCode || stop.postal_code;
if (postalCode) {
address += ` ${postalCode}`;
}
return address;
}
Playground
More functional approach:
const PROPS = ['street', 'placeName', 'place_name', 'city', 'province'] as const
const computeAddress = (stop: StrictUnion<AddressBookingStop | ShowBookingStop>) =>
PROPS.reduce((acc, elem) =>
stop[elem] ? `${acc}, ${stop[elem]}` : acc, ''
)
const withPostalCode = (address: string) =>
(stop: StrictUnion<AddressBookingStop | ShowBookingStop>) => {
const postalCode = stop.postalCode || stop.postal_code;
return postalCode ? `${address} ${postalCode}` : address
}
const formatAddress = (stop: StrictUnion<AddressBookingStop | ShowBookingStop>) =>
withPostalCode(computeAddress(stop))(stop)
Playground