DEV Community

loading...

Power of conditional types in Typescript

Gaurav Soni
Hi, My name is Gaurav Soni. I am Frontend Engineer.
・4 min read

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Let us create a conditional type to identify the error type,

type ErrorType<T extends {error: ApplicationError | Error}> = T['error'] extends ApplicationError ? ApplicationError : Error;
Enter fullscreen mode Exit fullscreen mode

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 screenshotserver error example
error example screenshoterror 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;    
}
Enter fullscreen mode Exit fullscreen mode

function overloading getError methodfunction overloading getError method
getError example with error screenshotgetError 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;
}
Enter fullscreen mode Exit fullscreen mode

getError example with server error screenshot
getError example with error screenshot
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;
Enter fullscreen mode Exit fullscreen mode

Screenshot for conditional types for error with additional error type

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/

Discussion (2)

Collapse
hamishwhc profile image
HamishWHC

Could you not just have type ErrorType<T extends ApplicationError | Error> = T?

Collapse
gauravsoni119 profile image
Gaurav Soni Author

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']