This is currently a limitation of TypeScript. See microsoft/TypeScript#33912 for a feature request to improve this.
The compiler is not particularly adept when reasoning about unspecified generic types, especially when the type is a conditional type. A type like NameOrId<T>, where T is not a specific type (such as the generic type parameter T inside the implementation of createLabel()) is more or less opaque to the compiler. It defers evaluating the type until the generic type parameters it depends on are specified. Until then, it cannot really verify that any specific type is assignable to it.
So all you get are these errors. For now there are only workarounds.
When the compiler is unable to verify that some expression is assignable to some type, but you are sure of it, you can use type assertions to just tell the compiler of your certainty. Type assertions will allow you to treat an expression x that the compiler sees as type X as an expression of type Y (x as Y), as long as the compiler thinks X and Y are sufficiently related. When it doesn't think so, you can usually force this to happen with an intermediate assertion related to both X and Y (such as x as any as Y or x as unknown as Y.
For createLabel() this could look like the following:
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
if (typeof idOrName === 'number') return { id: idOrName as number } as NameOrId<T>;
return { name: idOrName as string } as NameOrId<T>;
}
Another workaround is to continue using overloads, which allow for the separation between a function's call signature (possibly multiple of these) and its implementation signature. The compiler is fairly loose when checking the implementation signature against the call signatures, which tends to give a developer similar leeway as with type assertions, without having to write as Y all over the place. It has the same issue with type safety though, so when you do this you are essentially taking some of the responsibility for verifying safety away from the compiler.
In your case we have just the one call signature, a generic call signature whose return type is a conditional type. And for the implementation signature, we can widen T to the associated constraint, string | number:
function createLabel<T extends number | string>(idOrName: T): NameOrId<T>;
function createLabel(idOrName: number | string): NameOrId<number | string> {
if (typeof idOrName === 'number') return { id: idOrName };
return { name: idOrName };
}
The compiler is happy because it sees the implementation return type as IdLabel | NameLabel, which while not verifiably assignable to NameOrId<T>, is similar enough for the overload to be considered compatible.
Just to hammer the point home: this is a workaround. The compiler can't verify what you're doing exactly. You will be prevented from doing something totally bonkers like
function createLabelBonkers<T extends number | string>(idOrName: T): NameOrId<T> {
return new Date() as NameOrId<T>; // error
// Conversion of type 'Date' to type 'NameOrId<T>' may be a mistake
}
function createLabelBonkers2<T extends number | string>(idOrName: T): NameOrId<T>;
function createLabelBonkers2(idOrName: number | string): NameOrId<number | string> {
return new Date(); // error, not assignable to NameLabel | IdLabel
}
but if you make mistakes where the types involved are related to the correct ones, the compiler will not and cannot warn you:
function createLabelBad<T extends number | string>(idOrName: T): NameOrId<T> {
return (Math.random() < 0.5 ? { id: 123 } : { name: "abc" }) as NameOrId<T>
}
function createLabelBad2<T extends number | string>(idOrName: T): NameOrId<T>;
function createLabelBad2(idOrName: number | string): NameOrId<number | string> {
return Math.random() < 0.5 ? { id: 123 } : { name: "abc" };
}
It doesn't see the difference between the implementations of createLabel() and createLabelBad(). In neither case can it detect if the implementations are correct or not; the burden of doing this is on you, so be careful.
Playground link to code