Is it possible in TypeScript to have a variable that indicates the type of other variables?
Ultimately I would like to only check one variable stateIndicator
that is dependend on other variables to assume their types.
type A = {prop1: string}
type B = {prop2: number}
function isA(x: A | B): x is A {
return (x as A).prop1 !== undefined
}
function foo1(data: A | B) {
const stateIndicator = isA(data);
if (stateIndicator) {
// this is fine
const myString: string = data.prop1;
}
}
function foo2(data: A | B) {
const stateIndicator = isA(data) ? "stateA" : "stateB";
if (stateIndicator === "stateA") {
// here unless I check if isA(data) or do "data as A" the data.prop1 shows error
// Property 'prop1' does not exist on type 'B'
const myString: string = data.prop1;
}
}
No, what you're looking for isn't possible in TypeScript. See microsoft/TypeScript#46915 for a canonical answer.
In general, the TypeScript compiler cannot track all possible logical consequences of your code. That is, it doesn't do counterfactual reasoning like "in the body of foo2()
, if stateIndicator
is "stateA"
that must mean isA(data)
returned true
, since if had returned false
then stateIndicator
would be "stateB"
which is not equal to "stateA"
and there's no other way that stateIndicator
could be "stateA"
. So therefore data
must be an A
". It would be too expensive to do something like that in general.
For most cases any check you perform on one variable (like stateIndicator
) will, at most, affect the apparent type of that variable itself, and not other variables whose history might be entangled with it. So stateIndicator === "stateA"
narrows stateIndicator
itself, but nothing else. Even checking the property of an object will generally only affect the apparent type of that property, and not the apparent type of the parent object, unless the parent object is of a discriminated union type and the property checked is its discriminant.
There are a few cases where something like this does happen. TypeScript 4.4 introduced control flow analysis of aliased conditions and discriminants. That means if you assign the boolean
result of a yes-no type guard to a const
, or assign the discriminant property of a discriminated union to a const
, then subsequent checks of that const
will narrow the type of the related variable. This is what lets your foo1
implementation work; stateIndicator
is an aliased type guard boolean
. But in foo2
, stateIndicator
is neither an alias of a boolean
guard check, nor an alias of a discriminant property of a discriminated union. So no narrowing occurs.
Since what you're trying to do isn't possible you will have to work around it. In general the cleanest and most well-supported approach is to make the union into a discriminated union by adding a discriminant property to each member, and then checking the discriminant:
type A = { stateIndicator: "stateA", prop1: string }
type B = { stateIndicator: "stateB", prop2: number }
function foo(data: A | B) {
if (data.stateIndicator === "stateA") {
const myString: string = data.prop1;
} else {
const myNumber: number = data.prop2;
}
}
But obviously your use cases will drive the choice of workaround.