I have a problem with assignable values in one of my generic functions:

interface BuildArguments<T extends string> {
    type: T;
}

type PromiseResult<T> =
    T extends 'standalone' ? Promise<void> :
    T extends 'all' ? Promise<void> :
    Promise<void[]>;    

const foo: PromiseResult<'standalone'> = Promise.resolve();
const bar: PromiseResult<'all'> = Promise.resolve();
const baz: PromiseResult<'foo'> = Promise.resolve([]);

bundle({ type: 'foo' });

function bundle<T extends string>(buildArguments: BuildArguments<T>): PromiseResult<T> {
    switch (buildArguments.type) {
        case 'standalone':
            return Promise.resolve(); // error here, not assignable to PromiseResult<T>
        case 'all':
            return Promise.resolve(); // error here, not assignable to PromiseResult<T>
        default:
            return Promise.all([ // error here, not assignable to PromiseResult<T>
                Promise.resolve(),
                Promise.resolve()
            ]);
    }
}

The consts foo, bar and baz show that the conditional type works fine. the function call bundle({ type: 'foo' }) also correctly provides the type Promise<void[]> if you use the ts playground and hover over it. Why doesn't it work for the return values? I also tried if this is caused by TypeScript not being able to infer T by adding a kind: T argument to the function, but no changes. Asserting Promise.resolve() to PromiseResult<T> works fine.

1 Answers

3
Titian Cernicova-Dragomir On Best Solutions

Typescript will generally not let you do much with conditional types as long as they still have unresolved conditional types (as is T in case in PromiseResult<T>)

Moreover in this case you assume that narrowing buildArguments.type narrows T. It does not. Narrowing narrows a value, not a whole type parameter. This is can be no other way, consider this example:

function foo<T extends string | number>(a: T, b: T) {
    if(typeof a === "string") {
        // should b be string? No
    }
}

foo<string | number>(1, "");

Just because we narrowed one value that is of a type T has no bearing on other such variables.

The simplest solution is to use a separate implementation signature that is more permissive, while keeping the public signature with conditional types that is better for the caller:

function bundle<T extends string>(buildArguments: BuildArguments<T>): PromiseResult<T>
function bundle(buildArguments: BuildArguments<string>): PromiseResult<'standalone' | 'all'> | PromiseResult<string> {
    switch (buildArguments.type) {
        case 'standalone':
            return Promise.resolve(); 
        case 'all':
            return Promise.resolve(); 
        default:
            return Promise.all([ 
                Promise.resolve(),
                Promise.resolve()
            ]);
    }
}