typescriptgeneric-type-argument

Property does not exist on type 'something' in generic typed function


Why it does not work, when I am checking if the phone exists or not. The TypeScript should catch that and know if phone exists, then it must be AddressWithPhone interface.

Its been so long, I probably did not define the types correctly,

interface Address {
    street: string;
    zip: string;
    state: string;
    city: string;
}

interface AddressWithPhone extends Address {
    phone: string;
}


interface EditAddressProps<T = Address | AddressWithPhone> {
    address: T;
    onChange: (address: T) => unknown;
}

const something = <T,>(props: EditAddressProps<T>): string => {
    // Error: Property 'phone' does not exist on type 'T'
    if (props.address.phone) {
        return 'Its AddressWithPhone';
    }
    return 'Its Address without phone';
}

TS Playground:

https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgIIBN1QgZx8gbwChlTkcxsIwAucy0AcwG4SyAvYABzoqidZl6cSLwYgWbUgmBgAnmP4TWAXyJFQkWIhQYsuHAHVZACwAKJgPYgUEAB6QQ6fHux5CU5Fys3FAomrqmtDwSMgAouiyrgZmUJZcOAA8ACrIALxomG74AD5Z+njGYOY+EAB8HkJw2QZ0KYJk1gDCJnASEHQAFDWFOPUAlBmVAK4gANYglgDuIKrqCNYU5JYAttQmTBnIqQA05V1c8Yl0kdG1eHEJySnlA34Sw1VkwDDIh8c4AHS9OV-e1ggQ2IQiE2DAIygIGQAHIAJJgFwXIymCyAmGNUhqMHUSHQ+GIgo5ZDTUyWEZgLxlDEBIhAA


Solution

  • When you do <T = Address | AddressWithPhone> you are setting the default value of T to the union Address | AddressWithPhone. You haven't actually constrained the possible values of T so T still could be anything.

    You want to use extends to limit the possible values of T. This still doesn't work perfectly with a union because phone is not defined on Address, and properties must be defined on all members of a union in order to access them. We want a base type with all properties of Address required and phone optional. There's a lot of ways to define that, but here's one:

    type BaseAddress = Address & Partial<AddressWithPhone>
    
    const something = <T extends BaseAddress>(props: EditAddressProps<T>): string => {
    

    Playground Link