3

Let's imagine I have a function that I want to pass an argument to. My own condition type is based on the type of argument, but when I want to return the value based on interfaces, the function gives me an error.

This is the actual code:

interface IdLabel {
  id: number;
}

interface NameLabel {
  name: string;
}

type NameOrId<T extends string | number> = T extends number ? IdLabel : NameLabel;

function createLabel<T extends string | number>(idOrName: T): NameOrId<T> {
  if (typeof idOrName === "number") {
    return {
      id: idOrName
    };
  } else {
    return {
      name: idOrName
    };
  }
}

const a = createLabel(1);
const b = createLabel("something");

You can see the error in this picture:

enter image description here

this is the text error:

Type '{ id: T & number; }' is not assignable to type 'NameOrId<T>'.
Type '{ name: T; }' is not assignable to type 'NameOrId<T>'.
4

1 Answer 1

5

When a conditional type like NameOrId<T> depends on an unresolved type parameter like the T inside the body of createLabel(), the compiler defers evaluating it. Such a type is essentially opaque to the compiler, and therefore it is generally unable to verify that a specific value like {id: idOrName} is assignable to it.

When you check typeof idOrName, control flow analysis will narrow the apparent type of idOrName, but this does not narrow the type parameter T itself. If typeof idOrName === "string", idOrName is now known as being of type string (or T & string), but T is still possibly string | number, and is still an unresolved generic type parameter.

This is a known pain point of TypeScript. There is an open issue, microsoft/TypeScript#33912, asking for some way to allow control flow analysis to verify assignability to unresolved conditional types, especially as the return value of a function like you want here. Until and unless that issue is addressed, you'll have to work around it.


Workarounds:

Whenever you have a situation where you are certain that an expression expr is of type Type but the compiler is not, you can always use a type assertion to tell the compiler so: expr as Type, or, in cases where the compiler sees the type expr as completely unrelated to Type, you might have to write expr as any as Type or use some other intermediate assertion.

That would give you this:

function createLabelAssert<T extends string | number>(idOrName: T): NameOrId<T> {
    if (typeof idOrName === "number") {
        return {
            id: idOrName
        } as unknown as NameOrId<T>;
    } else {
        return {
            name: idOrName
        } as unknown as NameOrId<T>;
    }
}

This resolves the errors. Note that by making an assertion, you are taking responsibility for type safety. If you modified the typeof idOrName === "number" check to typeof idOrName !== "number", there would still be no compiler error. But you would be unhappy at runtime.


When you have a function whose return type can't be verified in the implementation, you could do the "moral equivalent" of a type assertion: an overload with a single call signature. Overload implementations are checked more loosely than regular functions, so you can get the same behavior as a type assertion without having to assert at each return line separately:

// call signature
function createLabel<T extends string | number>(idOrName: T): NameOrId<T>;
// implementation
function createLabel(idOrName: string | number): NameOrId<string | number> {
    if (typeof idOrName === "number") {
        return {
            id: idOrName
        };
    } else {
        return {
            name: idOrName
        };
    }
}

The call signature is the same, but now the implementation signature is just a mapping from string | number to IdLabel | NameLabel. Again, the compiler won't catch the problem where you check typeof idOrName !== "number" accidentally, so you need to be careful here too.

Playground link to code

Sign up to request clarification or add additional context in comments.

7 Comments

stackoverflow.com/users/2887218/jcalz Thank you so much for your help.
@jcalz, I completely agree with you about the overload solution, because in this case, NameOrId<string | number> is resolved (specified)IdLabel | NameLabel , and everything in the second Workarounds is quite good. But I have a question about the first Workaround. Could you help me to explain it?
@jcalz i use your first recommend workaround but instead of using intermediate assertion like as unknown as NameOrId<T> . i have asserted it to NameOrId<typeof idOrName> and surprisingly, It works.
@jcalz: this is my full code: Play ground
Comments sections on several-year-old answers aren't good places to bring up followup questions, especially if it takes multiple comments to even describe the issue. I'm not particularly inclined to delve into anything here, sorry.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.