Search code examples
typescripttypescript-genericsconditional-types

Why does conditional type inference in Typescript cause the type in my unref implementation to be classified as unknown?


I have implemented 2 versions of the Vuejs function "unref".
This has one parameter which can be of type Ref<T>:{ value: T } or anything else.
If the parameter gets a Ref unref or unref2 returns the Ref.value otherwise it simply returns the parameter.
The only difference between the functions is how I use the Typescript syntax.
Why does Typescript has an issue with the unref2 function?

Playground link

type Ref<Value> = { value: Value };

// Returns the type of Ref<TypeOfRef> if its a Ref else return T
type GetTypeOfMaybeRef<T> = T extends Ref<infer V> ? V : T;

// works
function unref<T>(maybeRef: Ref<GetTypeOfMaybeRef<T>>|GetTypeOfMaybeRef<T>): GetTypeOfMaybeRef<T> {
    if (maybeRef != null && typeof maybeRef === 'object' && "value" in maybeRef) {
        return maybeRef.value;
    } else {
        return maybeRef;
    }
}

// does not work, why?
// should be like unref.
// I know, T is just T.
// I did it to give ts a hint that T can be a Ref.
function unref2<T>(maybeRef: T extends Ref<infer Value> ? Ref<Value> : T): GetTypeOfMaybeRef<T> {
    if (maybeRef != null && typeof maybeRef === 'object' && "value" in maybeRef) {
        return maybeRef.value;
    } else {
        // why ts error? 
        // should it not be the same as unref?
        return maybeRef;
    }
}


const ref: Ref<number> = { value: 3 };
const numRef: Ref<number> = { value: 3 };
const num = unref(numRef)
const strRef: Ref<string> = { value: "blalba" };
const text = unref(strRef)


Solution

  • Deriving a generic type from an argument that has a conditional type is going to hit and miss. Sometimes Typescript can figure out simple cases, but when it gets complicated things will start to fail.

    It's best to avoid that situation altogether.


    That said, You are trying very hard here, and this can be much simpler:

    type Ref<Value> = { value: Value };
    
    function unref<T>(maybeRef: Ref<T> | T): T {
        if (maybeRef != null && typeof maybeRef === 'object' && "value" in maybeRef) {
            return maybeRef.value;
        } else {
            return maybeRef;
        }
    }
    

    There is no infer or even any conditional types required. unref takes either a Ref<T> | T and T is returned. Easy peasy.

    See playground


    And if you need GetTypeOfMaybeRef<T> still then you can do that without infer:

    type GetTypeOfMaybeRef<T> = T extends Ref<unknown> ? T['value'] : T;
    
    type A = GetTypeOfMaybeRef<{ value: number }> // number
    type B = GetTypeOfMaybeRef<string> // string
    

    See Playground