Home > Blockchain >  Typescript complains because of a conditional return type
Typescript complains because of a conditional return type

Time:09-08

Why does Typescript in the below scenario not let me return these values? To me, these are obviously of the correct type because of the type guard in the function body.

type Container = {
    name: string | number
}

function unpack<T extends Container>(x: T): T["name"] extends string ? "yes" : "no" {
    if (typeof x.name === "string") {
        return "yes"
    }
    return "no"
}

But TS tells me Type '"yes"' is not assignable to type 'T["name"] extends string ? "yes" : "no"'. and also Type '"no"' is not assignable to type 'T["name"] extends string ? "yes" : "no"'.

Typescript Playground

CodePudding user response:

This is currently a design limitation or missing feature of TypeScript. Currently there is no support for "narrowing" generic type parameters (maybe that would mean re-constraining them) via control flow analysis. There is a feature request at microsoft/TypeScript#33912 asking for something better here so that the compiler could possibly recognize code similar to your example as valid.


Currently the compiler treats generic conditional types as being essentially opaque; it defers evaluation until the generic types are specified. Inside the body of unpack(), the type checker just shrugs its proverbial shoulders and says it doesn't know what might be assignable to T["name"] extends string ? "yes" : "no", and complains about almost any assignment to that type. It doesn't really try to analyze what you're doing. You'd think it's easy to figure this out, but it's actually tricky in general.

Consider

const result = unpack({ name: Math.random() < 0.999 ? "string" : 123 });
// const result: "no"
console.log(result) // 99.9% "yes", oops

It turns out that you can't say that typeof x.name === "string" implies that T is any narrower than Container; it could be that T is Container. If so, then T["name"] extends string is actually false, in which case your return type is "no", but the implementation could well return "yes". Oops.

You could refactor to use a distributive conditional type, which will behave better here:

type YesNo<T> = T extends string ? "yes" : "no"
function unpack<T extends Container>(x: T): YesNo<T["name"]> {
    if (typeof x.name === "string") {
        return "yes"
    }
    return "no"
}

const result = unpack({ name: Math.random() < 0.999 ? "string" : 123 });
// const result: "yes" | "no"
console.log(result) // probably "yes", maybe "no", this is okay

But the compiler still doesn't see it as properly implemented, because it really has no idea what might be a valid return value. As I said, it's tricky. Implementing the kind of generic type analysis that is correct enough to be helpful and performant enough to be acceptable is not trivial.

For now if you want to suppress errors you need to use type assertions or their equivalent (like a single-call-signature overload) and move on:

function unpack<T extends Container>(x: T): YesNo<T["name"]> {
    if (typeof x.name === "string") {
        return "yes" as YesNo<T["name"]> // assert
    }
    return "no" as YesNo<T["name"]> // assert
}

recognizing that this lack of errors doesn't actually make anything safer:

function unpack<T extends Container>(x: T): YesNo<T["name"]> {
    if (typeof x.name !== "string") { // oops
        return "yes" as YesNo<T["name"]>
    }
    return "no" as YesNo<T["name"]>
}

Anyway, if you care about this, you might want to show support for microsoft/TypeScript#33912 by giving it a

  • Related