In computer science, a tagged union, also called a variant, variant record, choice type, discriminated union, disjoint union, sum type or coproduct, is a data structure used to hold a value that could take on several different, but fixed, types. (wikipedia)
Lets see how this type variance could be useful when converting the code below from JavaScript to TypeScript
class Scenario {
async getMetadata() {
try {
return {
success: true,
response: "some response here",
};
} catch (err) {
//log the error and return
return {
success: false,
errorMessage: "some message here",
};
}
}
async process() {
const response = await this.getMetadata();
if (response.success === true) {
//more code here..
} else {
//even more code here..
}
}
}
Same code in Typescript, this is a common approach
// Result type
type Result<T> = {
success: boolean;
response?: T; //in case of error response wont be there
errorMessage?: string; //in case of response error wont be there
};
declare function apiCall(): Promise<string>;
class Scenario {
async getMetadata<U>(): Promise<Result<U>> {
try {
const data = await apiCall();
return {
success: true,
response: data as U,
};
} catch (err) {
//log the error
return {
success: false,
errorMessage: "some message here",
};
}
}
async process() {
const result = await this.getMetadata<string>();
//additional check here
if (result.success === true && result.response) {
//more code here..
} else {
//even more code here..
}
}
}
The issue with above approach is that both response and errorMessage are optional and would need an additional check before accessing, this is because of 2 reasons:
In case of error response wont be there, so has to be optional
Similarly, in case of response, errorMessage wont be threre, again has to be optional
Although not bad, but could be improved using a discriminated union, here's the snippet below, first we convert Result<T> to a discriminated union, we will use success as literal type to discriminate the Result
type SuccessResult<T> = {
success:true,
response:T
}
type ErrorResult = {
success:false,
errorMessage:string
}
type Result<T> = SuccessResult<T> | ErrorResult;
Now the code could be re-written without an additional check.
class Scenario {
async getMetadata<U>(): Promise<Result<U>> {
try {
const data = await apiCall();
return {
success: true,
response: data as U,
};
} catch (err) {
//log the error
return {
success: false,
errorMessage: "some message here",
};
}
}
async process() {
const result = await this.getMetadata<string>();
if (result.success === true) {
// This will be infered as SuccessResult
} else {
//even more code here..
}
}
}
Hope this helps in understanding discriminated union in TypeScript.