mohammad nowresideh
mohammad nowresideh

Reputation: 158

Conditional type in typescript

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>'.

Upvotes: 3

Views: 1185

Answers (1)

jcalz
jcalz

Reputation: 330466

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

Upvotes: 5

Related Questions