Search code examples
typescriptconditional-types

Unable to access property from conditional typing


I want to have a function named Control that can be overloaded with props of two different types. This is the code I wrote. But it throws an error while trying to access specific properties of the prop. Any idea what I'm doing wrong?

interface IControl {
    title: string,
    description: string,
    type: 'button' | 'switch'
}

type TButtonVariant = 'contained' | 'outlined' | undefined

interface IControlWithButton extends IControl {
    buttonText: string,
    buttonVariant: TButtonVariant
}

interface IControlWithSwitch extends IControl {
    isOn: boolean
}

type GetExactProp<T> = T extends { isOn: boolean } ? IControlWithSwitch : IControlWithButton;


function Control<T extends IControlWithButton | IControlWithSwitch>(props: GetExactProp<T>): void {
    console.log(props.buttonText) // Property 'buttonText' does not exist on type 'GetExactProp<T>'
    console.log(props.isOn) // Property 'isOn' does not exist on type 'GetExactProp<T>'
}

const ControlWithButtonProps: IControlWithButton = {
    title: 'Ola!',
    description: 'Section description',
    buttonText: 'I am a button',
    buttonVariant: 'contained',
    type: 'button'
}

const ControlWithSwitchProps: IControlWithSwitch = {
    title: 'Ola!',
    description: 'Section description',
    isOn: false,
    type: 'switch'
}

Control(ControlWithButtonProps)
Control(ControlWithSwitchProps)

Try this out in TypeScript Playground


Solution

  • The reason why the error appears is:

    No value will be assignable to an unresolved conditional type (a conditional type that still depends on a free generic type variable)

    From: https://stackoverflow.com/a/52144866/12397250

    That's why Typescript has no idea what type props inside the function is. What you can do is to put a type guard in the function, and the generic isn't really necessary here

    function Control(props: IControlWithButton | IControlWithSwitch): void {
        if ("isOn" in props) {
            props; // IControlWithSwitch
            props.isOn; // boolean
            props.buttonText; // ERROR
        } else {
            props; // IControlWithButton
            props.isOn; // ERROR
            props.buttonText; // string
        }
    }
    

    Or create a user defined type guard:

    function isControlWithSwitch(control: IControlWithButton | IControlWithSwitch): control is IControlWithSwitch {
        return "isOn" in control;
    }
    
    function Control(props: IControlWithButton | IControlWithSwitch): void {
        if (isControlWithSwitch(props)) {
            props; // IControlWithSwitch
            props.isOn; // boolean
            props.buttonText; // ERROR
        } else {
            props; // IControlWithButton
            props.isOn; // ERROR
            props.buttonText; // string
        }
    }