2
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;
}

playground

TypeScript is complaining:

enter image description here

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?

mpen
  • 272,448
  • 266
  • 850
  • 1,236

2 Answers2

2

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!

geisterfurz007
  • 5,292
  • 5
  • 33
  • 54
  • The `as` solution doesn't look too bad. Would be nice if TS had a more strict version of "as" that only let you cast to subtypes so you couldn't accidentally botch it quite as bad. Oh well, that works for me. Thanks! – mpen Sep 05 '21 at 00:15
  • 1
    The compiler *does* yell at you if you try casting to something that isn't possible ([playground](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgILIN4Chm+XALmQGcwpQBzAbiwF8stRJZEUAhTHPAIyNPJDU6DJtHhJkAYU55kCPmUo16WGAFcQCMMAD2IfIKgBPSXFIAKOAHkobIugA+yNgEoZeBHuI6ANhAB0PjoU5pY2HGZSLv4ILspYQA))! You could still force it to believe you by using `as unknown as C` in that example however but that doesn't apply to your case. – geisterfurz007 Sep 05 '21 at 17:27
1

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. enter image description here

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

  • 1
    That `StrictUnion` helper is really cool! I've wanted something like that several times now. I almost wonder if they should make it a compiler flag to make `|` behave that way. – mpen Sep 05 '21 at 21:11
  • Yest it is cool. But some times it is not enough. See this : find Second part ,https://dev.to/captainyossarian/how-to-type-react-props-as-a-pro-2df2 – captain-yossarian from Ukraine Sep 05 '21 at 21:33