Hafiz Temuri
Hafiz Temuri

Reputation: 4122

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

Upvotes: 2

Views: 5384

Answers (2)

Here is alternative way to do it:

type AddressBase = {
    street: string;
    zip: string;
    state: string;
    city: string;
};

type Address =
    | AddressBase
    | AddressBase & {
        phone: string;
    }

type EditAddressProps<T> = {
    address: T;
    onChange: (address: T) => unknown;
}

const withPhone = (props: EditAddressProps<any>): props is EditAddressProps<AddressBase & {
        phone: string;
    }> => Object.prototype.hasOwnProperty.call(props.address, 'phone');

const withoutPhone = (props: EditAddressProps<any>): props is EditAddressProps<AddressBase> =>
    !withPhone(props)


const something = < T extends Address>(props: EditAddressProps<T>) => {
    if (withPhone(props)) {
        const phone = props.address.phone; // string
        props.onChange({
            street: '123',
            zip: 'AB12345',
            state: 'TX',
            city: 'Dallas',
            phone: 'sdf', // phone is required here
        })
        return 'hello'
    }

    if (withoutPhone(props)) {
        const x = props.address // no phone
        props.onChange({
            street: '123',
            zip: 'AB12345',
            state: 'TX',
            city: 'Dallas',
            phone: 'sdf' // expected error, phone property is redundant here
        })
    }

    const defaultCase = props; // EditAddressProps<T>


    return 'default'

}

Playground link

If you have a deal with union types with generics, I would suggest to use typeguards, because TS is unable to narrow it as you would expect

Upvotes: 1

Linda Paiste
Linda Paiste

Reputation: 42188

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

Upvotes: 3

Related Questions