You can change pick()'s call signature to be a generic method whose parameter is of a generic type constrained to the union of your different desired input types, and whose return type is a conditional type which chooses the output type based on the input type:
declare class Selection {
pick<T extends Car | Human>(what: T): T extends Car ? Garage : House;
}
This behaves the same for your example calls:
const selection = new Selection();
selection.pick(new Car()).lots; // compiler successfully autocompletes
selection.pick(new Human()).floors // compiler successfully autocompletes
selection.pick(new NotAllowed()); // compiler error
Unfortunately your implementation will not be seen as type safe; the type T will be unspecified inside of the body of pick(), and so the compiler does not and cannot verify that T is assignable to Car even if what instanceof Car is true:
pick<T extends Car | Human>(what: T): T extends Car ? Garage : House {
if (what instanceof Car) {
return new Garage(); // error!
// Type 'Garage' is not assignable to type 'T extends Car ? Garage : House'
} else if (what instanceof Human) {
return new House(); // error!
// Type 'House' is not assignable to type 'T extends Car ? Garage : House'.
} else {
throw Error('Not supported')
}
}
Instead of going on about why this happens, I will just direct you to microsoft/TypeScript#33912, the relevant open feature request to improve language support for implementing generic functions returning conditional types.
For now you will need to do something like a type assertion:
pick<T extends Car | Human>(what: T): T extends Car ? Garage : House {
if (what instanceof Car) {
return new Garage() as T extends Car ? Garage : House;
} else if (what instanceof Human) {
return new House() as T extends Car ? Garage : House;
} else {
throw Error('Not supported')
}
}
or equivalently, a single call-signature overload:
pick<T extends Car | Human>(what: T): T extends Car ? Garage : House;
pick(what: Car | Human) {
if (what instanceof Car) {
return new Garage();
} else if (what instanceof Human) {
return new House();
} else {
throw Error('Not supported')
}
}
I know you said you didn't want overloads, but this is usually my solution since adding type assertions everywhere is messier and more tedious.
Playground link to code