I want my code editor to infer the type of extraData
based on the value of error
which is being narrowed by the if statement:
export enum ErrorCodes {
Unknown = 'UNKWN',
BadRequest = 'BDREQ',
}
interface ExtraData {
[ErrorCodes.Unknown]: string,
[ErrorCodes.BadRequest]: number,
}
let error: ErrorCodes;
let extraData: ExtraData[typeof error];
if(error === ErrorCodes.Unknown){
console.log(extraData) // on hover VS code says it's a string | number
}
But if I put the extraData
type assigment inside the if block then it works:
export enum ErrorCodes {
Unknown = 'UNKWN',
BadRequest = 'BDREQ',
}
interface ExtraData {
[ErrorCodes.Unknown]: string,
[ErrorCodes.BadRequest]: number,
}
let error: ErrorCodes;
if(error === ErrorCodes.Unknown){
let extraData: ExtraData[typeof error];
extraData // on hover VS code says it's a string
}
I don't want to do that in every conditional block since there will be dozens of error codes and I can't validate using typeof
since we will use objects as the actual extra data
CodePudding user response:
If error
and extraData
are just two variables of the type ErrorCodes
and ExtraData[typeof error]
respectively, there's nothing you can do. ErrorCodes
is a union, and so ExtraData[typeof error]
is also a union type. Two union types are always treated as independent or uncorrelated with each other. The type typeof error
is just ErrorCodes
and doesn't "remember" anything about error
. There is no way to say in types that two separately-declared variables are of correlated union types.
So if you'd like error
to be something you can check and have the check have an effect on the apparent type of extraData
, you can't declare them separately. Instead they should be declared together as fields of a single discriminated union type. Here's one way to express such a type:
type ErrorAndExtra = { [K in keyof ExtraData]:
{ error: K, extraData: ExtraData[K] }
}[keyof ExtraData]
/* type ErrorAndExtra = {
error: ErrorCodes.Unknown;
extraData: string;
} | {
error: ErrorCodes.BadRequest;
extraData: number;
} */
The ErrorAndExtra
type is a union where each member has a discriminant error
property of literal type, and an extraData
property whose type depends on the type of the error
property.
If you had a variable of type ErrorAndExtra
, you could get the narrowing behavior you're looking for:
declare const e: ErrorAndExtra;
if (e.error === ErrorCodes.Unknown) {
console.log(e.extraData.toUpperCase()) // okay
} else {
console.log(e.extraData.toFixed()) // okay
}
That might be sufficient for you; still, you can get the same behavior with two separate variables, as long as you destructure them from a value of the discriminated union type:
declare const { error, extraData }: ErrorAndExtra;
if (error === ErrorCodes.Unknown) {
console.log(extraData.toUpperCase()) // okay
} else {
console.log(extraData.toFixed()) // okay
}
This also works; TypeScript understands that error
and extraData
came from the ErrorAndExtra
discriminated union, and so the check on error
has the effect on extraData
you're expecting.
Again, this only works because the declaration of error
and extraData
is specifically following a pattern that TypeScript supports for this purpose. If you change the pattern, it could break. For example:
declare let { error, extraData }: ErrorAndExtra;
if (error === ErrorCodes.Unknown) {
console.log(extraData.toUpperCase()) // error
} else {
console.log(extraData.toFixed()) // error
}
Here we changed the declaration from a const
to a let
. And that breaks the spell. Since let
variables can be reassigned, the compiler would have to track such assignments before it could verify that a check on error
should have any effect on the apparent type of extraData
, and it's not considered worth the extra work for the compiler to do this.