One of the most loved type system in the javascript world is the typescript type system. It comes with a lot of features. One of the features that we are discussing today is called conditional types.
Conditional types are a lot like a javascript's ternary operator. Based on the condition, Typescript will decide which type can be assigned to the variable. Conditional types mostly work with generics.
A few words about generics
Generics are created to work over a variety of types. Consider the example from typescript website,
function identity<T>(arg: T): T {
return arg;
}
Here the T is representing the generic type. Typescript decides the value of T dynamically either by type inferencing or we can tell typescript specifically the type. For example,
const output = identity('myString'); // typeof output is string
Const output = identity<string>('myString'); // type is string
Back to conditional types
Now let's discuss the conditional types. As we said earlier, conditional types are more like a ternary operator in javascript, below is the example,
type IamString<T> = T extends string ? 'I am string': 'I am not string';
type str = IamString<string>; // "I am string"
type notStr = IamString<number>; // "I am not string"
As we can see in the above example, if we pass a string to the type IamString, we will get "I am string", otherwise it's giving "I am not string". On the other way, you can also think of conditional types as adding constraints to the generic types. T is extending the string is a constraint here.
Error handling example
In this article, we will take an example of error handling. Consider we are handling the errors in our application. Let say we have two types of errors in the application. 1) Application Error - Error specific to application 2) Error - normal javascript error.
Let say we abstract the ApplicationError class,
abstract class ApplicationError {
abstract status: number;
abstract message: string;
}
Our custom errors will extend this abstract class and add their implementation. For example,
class ServerError extends ApplicationError {
status = 500;
constructor(public message: string) {
super();
}
}
Let us create a conditional type to identify the error type,
type ErrorType<T extends {error: ApplicationError | Error}> = T['error'] extends ApplicationError ? ApplicationError : Error;
Now if you try to pass an object which has an error that extends ApplicationError, we will get the type ApplicationError otherwise we will get the Error type,
server error example
error example
We can also use this type(ErrorType) as a return type of function. Consider a function that extracts an error from the object and returns that error. The one way to implement that function is to use function overloading,
function getError(response: {error: ApplicationError}): ApplicationError;
function getError(response: {error: Error}): Error;
function getError(response: {error: ApplicationError | Error}): ApplicationError | Error {
if (response.error instanceof ApplicationError) {
return response.error;
}
return response.error;
}
function overloading getError method
getError example with error screenshot
In the screenshots, Typescript can identify the type of error for us. But consider in future you are having four types of error in the application. Then you need to overload the getError function two more times which might be annoying.
Now Let's implement the same thing with the condition types,
type ErrorType<T extends {error: ApplicationError | Error}> = T['error'] extends ApplicationError ? ApplicationError : Error;
function getError<T extends { error: ApplicationError | Error }>(response: T): ErrorType<T> {
if (response.error instanceof ApplicationError) {
return <ErrorType<T>>response.error;
}
return <ErrorType<T>>response.error;
}
You can see that we have the same results but without doing overloading. The only thing is we need to tell the typescript compiler the return type of function explicitly by doing >. You can also use any type and typescript will give the same result.
Now consider you are going to add one error type to the application, you can simply nest the ternary operator to accommodate it.
type MyCustomError = "CustomError";
type ErrorType<
T extends { error: ApplicationError | MyCustomError | Error }
> = T["error"] extends ApplicationError
? ApplicationError
: T["error"] extends MyCustomError
? MyCustomError
: Error;
Summary
The conditional types might look difficult to understand the first time but it's worth putting effort into exploring the usage of conditional types and using it.
Further Reading:-
https://medium.com/r/?url=https%3A%2F%2Fwww.typescriptlang.org%2Fdocs%2Fhandbook%2F2%2Fconditional-types.html
https://artsy.github.io/blog/2018/11/21/conditional-types-in-typescript/
Top comments (2)
Could you not just have
type ErrorType<T extends ApplicationError | Error> = T
?Yes, we can also use something similar. In our case, T is an object having error property. If you try to use this, you will get an object
(type e = {error: ServerError;})
instead of either ApplicationError or Error. You can try like,
type ErrorType<T extends {error: ApplicationError | Error}> = T['error']