The problem you're running into is that generic constraints of the form T extends RGT
does not mean "T
must be exactly either RG
or RGIC
". All it implies is that T
is compatible with RGT
, meaning that it is a subtype of or assignable to RGT
. There are subtypes of RGT
which are more specific than either RG
or RGIC
, such as:
interface Whaa {
combinator: 'or',
rules: R[],
brainCellCount: number;
}
const whaa: Whaa = {
combinator: 'or',
rules: [],
brainCellCount: 86e9
}
The Whaa
interface is assignable to RG
; every Whaa
is a valid RG
. But a Whaa
has a combinator
which must be "or"
, and it also has an additional brainCellCount
property of type number
.
If your function f
has a call signature like this:
declare const f: <T extends RGT>(r: T) => T;
you're saying that the return type of f
will be exactly the same as the type of the passed-in r
. And therefore the following call will produce a result of type Whaa
also:
const hmm = f(whaa);
// const hmm: Whaa
And if so, then this will be fine:
hmm.brainCellCount.toFixed(2); // no compiler error
But it's not fine with your implementation of f
:
const f = <T extends RGT>(r: T): T => {
if ('combinator' in r) {
return { combinator: 'and', rules: [] }; // error
}
return { rules: [] }; // error
}
hmm.brainCellCount.toFixed(2); // no compiler error
// BUT AT RUNTIME: brainCellCount is undefined !!
Now the error should hopefully makes sense. You're returning a value like {combinator: "and", rules: []}
and claiming that it's the same type T
as that of r
. But the compiler is saying that such a value, while assignable to RGT
, might not be assignable to T
.
There are different ways to handle this. Since you really just want to say that if the input is an RG
then so is the output, and if the input is an RGIC
then so is the output, then the easiest way is probably to make f
an overloaded function with two call signatures:
// call signatures
function f(r: RG): RG;
function f(r: RGIC): RGIC;
// implementation
function f(r: RGT): RGT {
if ('combinator' in r) {
return { combinator: 'and', rules: [] };
}
return { rules: [] };
}
This compiles with no error. Although you should be careful: it's not really guaranteed to be type safe; the compiler won't catch errors like if ('cobminator' in r)
because overload implementations are checked loosely. As long as each return
statement applies to some call signature the compiler will be happy, even if you get the wrong one. So just double and triple check that your implementation works for each call signature. It does, so that's good.
Let's see how it works when we call it:
const hmm = f(whaa);
// const hmm: RG
hmm.brainCellCount //<-- now this is a compiler error
Looks good. Now the compiler accepts that whaa
is an RG
and the output type is also an RG
. If you try to access the brainCellCount
property of hmm
, the compiler now complains that there is no such property on RG
.
Another similar way to handle this is to continue to use a generic function, but have the return type be a conditional type like this:
const f = <T extends RGT>(r: T): T extends RG ? RG : RGIC => {
if ('combinator' in r) {
return { combinator: 'and', rules: [] } as any;
}
return { rules: [] } as any;
}
That return type T extends RG ? RG : RGIC
captures your intent of widening the return type to RG
if the input is some subtype of RG
, or otherwise widening to RGIC
. Note that in both return
statements I had to use a type assertion to any
(I could have done as T extends RG ? RG : RGIC
instead) to make it compile. That's because the compiler is really not able to understand when some specific value is assignable to a conditional type depending on an unspecified generic type parameter. There's an open feature request at microsoft/TypeScript#33912 asking for something better there, but I don't know when or if it will improve. For now, type assertions or overloads are the ways to go.
Anyway, you can see that it behaves the same:
const hmm = f(whaa);
// const hmm: RG
hmm.brainCellCount // error
when you call f(whaa)
the compiler infers that T
is Whaa
, and then evaluates T extends RG ? RG: RGIC
to be RG
. And so hmm
is of type RG
as desired, which is not known to have a brainCellCount
property. And you get the compiler error where expected.
Playground link to code