Reputation: 158
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:
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
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.
Upvotes: 5