Reputation: 282845
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;
}
function formatAddress(stop: 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;
}
TypeScript is complaining:
But I have stop.placeName || stop.place_name
there, so one of those two properties is guaranteed to exist.
Is there any way to make TS understand this?
Upvotes: 4
Views: 762
Reputation: 33041
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;
}
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)
Upvotes: 2
Reputation: 5874
TypeScript does not allow you to access properties of a value that aren't 100% there.
If you just want to suppress all warnings on that line, you can use the // @ts-ignore
comment introduced in TS 2.6.
If you want a little less bruteforce method of silencing the compiler, you can pretend to have information you don't really have by casting to the respective types:
function formatAddress(stop: AddressBookingStop|ShowBookingStop): string {
let address = [stop.street || (stop as AddressBookingStop).placeName || (stop as ShowBookingStop).place_name, stop.city, stop.province].filter(s => s).join(', ');
const postalCode = (stop as AddressBookingStop).postalCode || (stop as ShowBookingStop).postal_code;
if (postalCode) {
address += ` ${postalCode}`;
}
return address;
}
If you want to do make the compiler super happy without cheating (but at the cost of some runtime logic, sadly), you can use a manual typeguard like this:
function isShowBookingStop(stop: AddressBookingStop|ShowBookingStop): stop is ShowBookingStop {
return Boolean((stop as ShowBookingStop).id);
}
function formatAddress(stop: AddressBookingStop|ShowBookingStop): string {
const street = stop.street || (isShowBookingStop(stop) ? stop.place_name : stop.placeName);
let address = [street, stop.city, stop.province].filter(s => s).join(', ');
const postalCode = isShowBookingStop(stop) ? stop.postal_code : stop.postalCode;
if (postalCode) {
address += ` ${postalCode}`;
}
return address;
}
The return value of stop is ShowBookingStop
tells the compiler that if the returned value is true, the input was of type ShowBookingStop
. You can then use that information in the formatAddress
function to determine which property to use. Note that you will have auto-completion in the falsy branch of the ternary operator as well because the compiler understands that if the stop is not a ShowBookingStop
, it has to be an AddressBookingStop
.
Which of these options you use, is entirely up to you!
Upvotes: 3