Advanced Types

This is a set of complex examples on practical usage of the TypeScript type system. If you have not mastered the basics of types or are unfamiliar with the utility types, it may be challenging to follow along. However, regardless of your TypeScript comprehension level, you should be able to easily adapt these examples for your own purposes.

Object utility types

This is series of illustrative exercises on composing utility types into much more sophisticated utility types for working with objects.

Filtering an interface by value

When filtering an interface, you will often find yourself using the Pick utility type, which filters an object type by keys.

Suppose we wanted a ValuePick utility which picks based on the value type. Let's start by setting up an mapped type that effectively constructs an identical type to the one we pass in as T.

Even though this didn't really accomplish anything, it helps establish a starting point that we can expand on to construct the type. The next step is to examine T[Key] with extends, to see if it is fully-enclosed by the value type V.

The issue here is that the resulting type still has all the original object's keys, with values that don't match type V rewritten as never. This creates a compilation error, even though there is no possible value that can be assigned to the key.

Rather than retrieve T[Key] (the value type), let's retrieve Key (the key from our object T).

We can now perform type flattening to retrieve the union of all value types for the resulting object, which will automatically filter out never.

Now that we can construct a union of keys that we are interested in, we can pass them as the second type parameter of Pick (the first parameter being our object type T).

We can optionally combine these two types into a single ValuePick type.

Now that you have seen how to construct a complex type, see if you can read the type below and comprehend how it works.

This utility type uses a technique for checking strict equality of two types.

strict type equality
[A] extends [B] ? [B] extends [A] ? equal : never : never

Schema validation

Suppose we want to validate arbitrary JSON objects that are sent to a web server from a messaging application. One possibility is to write a type predicate that performs some runtime validation on the object.

Loading TypeScript...

The first thing to notice is that sending the string null to our server causes unexpected behavior. Rather than throw an Error, it continues to the step of validating whether result.sender is a string. This is because typeof null returns "object" - a subtle bug that is difficult to spot.

This method will also grow cumbersome as the interface for Message grows larger. By maintaining a separate interface and type predicate function for validating that interface, we are effectively replicating an error-checking process between compile time and runtime. With some sophisticated type definitions, we can actually write an object that simultaneously produces a TypeScript interface and runtime type checker.

Using type identifiers

The first step is to define some unique type identifiers. This example uses strings that closely resemble the value of typeof, but you can use any identifier that TypeScript can distinguish.

We can add more unique identifiers to SchemaType and expand the ternary logic for the SchemaValue type to include them. Notice that we are including never as the final result if none of the types were matched.

  • creating a Schema object
  • tying it all together into a validator
  • deep inference on nested objects and types

Creating a Schema object

Let's define a Schema interface to store the data representation of a TypeScript type. For the sake of clarity, there are only four types - it is left as an exercise for the reader to add more.

Was this page helpful?