DEV Community

Cover image for TypeScript: The Power of Never πŸ™…β€β™‚οΈ
Daniel Marinho
Daniel Marinho

Posted on

TypeScript: The Power of Never πŸ™…β€β™‚οΈ

When creating new types or interfaces in your projects, you may come up with crazy solutions to solve a problem to ensure property values that actually can be handled at the type level of your code.

Drafting

Just recently, I was creating my personal site and experimenting with new things and ideas, and one of them was to have a mutable type to use as the propType for a component that was responsible for handling gradient headers on a page.

The main idea is that you can use custom colors to set your left and right edges, but also, have the option to define a custom linear gradient if you like. The catch is, they can't co-exist at the same time.

In our drafted idea, our type would be something like this:

type GradientTitle = {
   name: string;
   leftColor?: string;
   rightColor?: string;
   customColor?: string;
}
Enter fullscreen mode Exit fullscreen mode

This way, we can set our gradient with custom values, like so (using pug 🐢):

...
GradientTitle(
   name="My Awesome Name"
   leftColor="#a163f1"
   rightColor="#3498ea"
)
...
Enter fullscreen mode Exit fullscreen mode

If we want to use a custom gradient, we could do it, like so:

GradientTitle(
   name="My Awesome Name"
   customColor="linear-gradient(45deg,#a163f1,#6363f1 22%,#3498ea 40%,#40dfa3 67%, rgba(64, 223, 163, 0));"
)
Enter fullscreen mode Exit fullscreen mode

The problem here is that there is nothing that blocks us from having the two options at the same time. Moreover, what should happen if we have both options set?

// Gradient hell
GradientTitle(
   name="My Awesome Name"
   leftColor="#a163f1"
   rightColor="#3498ea"
   customColor="linear-gradient(45deg,#a163f1,#6363f1 22%,#3498ea 40%,#40dfa3 67%, rgba(64, 223, 163, 0));"
)
Enter fullscreen mode Exit fullscreen mode

Ideias

First off, having everything optional does not seem reasonable. What if I say I have this for "leftColor" and nothing, for "rightColor"? That would have us creating business logic inside the component just to handle what properties have values or not and then deciding if we should use leftColor and rightColor, or customColor.

We would need to create a function just to validate our props to decide what to use, something like this:

const isCustomColor: boolean = (props: GradientTitle) =>
   typeof props.leftColor !== 'string' && typeof props.rightColor !== 'string' && typeof props.customColor === 'string';

...
if (isCustomColor(props)) {
 // Use props.customColor
} else {
 // Use props.leftColor and props.rightColor
}
Enter fullscreen mode Exit fullscreen mode

That is one way of resolving this problem at runtime. However, wouldn't it be nicer to solve this issue at the type level? I mean, let's make TS do this job for us, shall we?

In our case, we have our GradientTitle type already defined:

// remember
type GradientTitle = {
   name: string;
   leftColor?: string;
   rightColor?: string;
   customColor?: string;
}
Enter fullscreen mode Exit fullscreen mode

First off, let's decide what is mandatory for this type. The name seems to be the key value for this component to exist, so let's have it as our base type:

type GradientProp = {
   name: string;
}
Enter fullscreen mode Exit fullscreen mode

Nice! Now, we should handle how we should address the colors, in a way we can have the option of using left and right or using a custom value, but never both. See what I did there? never? πŸ˜‚

The NEVER type

Never in TypeScript deserves a whole new topic, but essentially, it's a way to define a type that should never be present. Simply put, JavaScript does not have a primitive value of type "never". We have string, number, bigint, boolean, symbol, null, and undefined. So, when it comes to the type level when a property of type never exists in the scope, the TS throws an error informing us that "this shouldn't be here".

Phew! That's a lot of information in a short paragraph. With that said, let's use this never to our advantage.

Back to the example

There are two options available in our scenario: one is having both left and right colors defined but never having customColor, and the other is to have the customColor defined but never have left and right colors set. That would leave us with this:

type GradientProps = {
   name: string;
}

type LeftAndRightColors = {
   leftColor: string;
   rightColor: string;
   customColor?: never;
}

type CustomColor = {
   customColor: string;
   rightColor?: never;
   leftColor?: never;
}
Enter fullscreen mode Exit fullscreen mode

You might be asking, "Why are there optional values in these types?". I got you! Let's think about that.

In the type LeftAndRightColors, we should set the left and right colors. If customColor wasn't optional, the TS would yell at us saying that we should have a value of type 'never' to this property. However, if you assign anything to customColor in this object, you'll get a type error.

type LeftAndRightColors = {
   leftColor: string;
   rightColor: string;
   customColor: never;
}

const LRColors: LeftAndRightColors = {
      // ^error: Property 'customColor' is missing in type.
   leftColor: "#000000",
   rightColor: "#111111"
}
Enter fullscreen mode Exit fullscreen mode

Now, if we have it as an optional property, the TS would not yell at us when missing. Moreover, as we have its type defined as never, if we set anything to it we also get an error.

type LeftAndRightColors = {
   leftColor: string;
   rightColor: string;
   customColor?: never;
}

const LRColors: LeftAndRightColors = {
   leftColor: "#000000",
   rightColor: "#111111",
   customColor: "anything"
   // ^erro: Types of property 'customColor' are incompatible.
}
Enter fullscreen mode Exit fullscreen mode

Now, we have two types defining the two scenarios we may encounter. However, we can still improve this.

Separation of concerns

It does not seem reasonable to have these properties repeating themselves within these two types, LeftAndRightColor and CustomColor. We should have their values defined as normal and then have new types to negate the property value when necessary. Like so:

type LeftAndRightColors = {
   leftColor: string;
   rightColor: string;
}

type CustomColor = {
   customColor: string;
}

type CustomNotAllowed = {
   customColor?: never;
}

type LRNotAllowed = {
   leftColor?: never;
   rightColor?: never;
}
Enter fullscreen mode Exit fullscreen mode

With that, we can use our intersection operator to define the negatives in our types, just by intersecting them, like so:

type LeftAndRightColors = {
   leftColor: string;
   rightColor: string;
} & CustomNotAllowed;

type CustomColor = {
   customColor: string;
} & LRNotAllowed;
Enter fullscreen mode Exit fullscreen mode

Using Type Operations

In TypeScript we have what we call Union Types and Intersection Types. Union Types are types that are combined using the | symbol and Intersection Types are types that are combined with the & symbol. The idea is that you can create a new type by combining some existing types, making their type rules co-exist.

When we intersect two types, it creates a new type combining all its property rules as a result. In addition, we can easily understand whats going on by renaming our boundaries.

type GradientProps = {
   name: string;
}

type LRNotAllowed = {
   leftColor?: never;
   rightColor?: never;
}

type CustomNotAllowed = {
   customColor?: never;
}

type LeftAndRightColors = {
   leftColor: string;
   rightColor: string;
} & CustomNotAllowed;

type CustomColor = {
   customColor: string;
} & LRNotAllowed;

export type GradientTitle = GradientProps & (LeftAndRightColors | CustomColor);
Enter fullscreen mode Exit fullscreen mode

Finally, we have TypeScript handling at the type level of what properties are allowed to exist based on the values. Also, we can easily understand what each type is responsible for and combine its effects using type operators.

Conclusion

The never type in TypeScript can be very helpful when defining boundaries on property values. Combined with operators, we can create very powerful types that better handle specific rules of complex components.

Top comments (0)