Marius Schulz
Marius Schulz
Front End Engineer

Generic Parameter Defaults in TypeScript

TypeScript 2.3 implemented generic parameter defaults which allow you to specify default types for type parameters in a generic type.

In this post, I want to explore how we can benefit from generic parameter defaults by migrating the following React component from JavaScript (and JSX) to TypeScript (and TSX):

class Greeting extends React.Component {
  render() {
    return <span>Hello, {this.props.name}!</span>;
  }
}

Don't worry, you don't have to know React to follow along!

#Creating a Type Definition for the Component Class

Let's start by creating a type definition for the Component class. Each class-based React component has the two properties props and state, both of which have arbitrary shape. A type definition could therefore look something like this:

declare namespace React {
  class Component {
    props: any;
    state: any;
  }
}

Note that this is a vastly oversimplified example for illustrative purposes. After all, this post is not about React, but about generic type parameters and their defaults. The real-world React type definitions on DefinitelyTyped are a lot more involved.

Now, we get type checking and autocompletion suggestions:

class Greeting extends React.Component {
  render() {
    return <span>Hello, {this.props.name}!</span>;
  }
}

We can create an instance of our component like this:

<Greeting name="World" />

Rendering our component yields the following HTML, as we would expect:

<span>Hello, World!</span>

So far, so good!

#Using Generic Types for Props and State

While the above example compiles and runs just fine, our Component type definition is more imprecise than we'd like. Since we've typed props and state to be of type any, the TypeScript compiler can't help us out much.

Let's be a little more specific and introduce two generic types Props and State so that we can describe exactly what shape the props and state properties have:

declare namespace React {
  class Component<Props, State> {
    props: Props;
    state: State;
  }
}

Let's now create a GreetingProps type that defines a single property called name of type string and pass it as a type argument for the Props type parameter:

type GreetingProps = { name: string };

class Greeting extends React.Component<GreetingProps, any> {
  render() {
    return <span>Hello, {this.props.name}!</span>;
  }
}

Some terminology:

  • GreetingProps is the type argument for the type parameter Props
  • Similarly, any is the type argument for the type parameter State

With these types in place, we now get better type checking and autocompletion suggestions within our component:

TypeScript autocompletion list for statically typed React component

However, we now must provide two types whenever we extend the React.Component class. Our initial code example no longer type-checks correctly:

// Error: Generic type 'Component<Props, State>'
// requires 2 type argument(s).
class Greeting extends React.Component {
  render() {
    return <span>Hello, {this.props.name}!</span>;
  }
}

If we don't want to specify a type like GreetingProps, we can fix our code by providing the any type (or another dummy type such as {}) for both the Props and State type parameter:

class Greeting extends React.Component<any, any> {
  render() {
    return <span>Hello, {this.props.name}!</span>;
  }
}

This approach works and makes the type checker happy, but: Wouldn't it be nice if any were assumed by default in this case so that we could simply leave out the type arguments? Enter generic parameter defaults.

#Generic Type Definitions with Type Parameter Defaults

Starting with TypeScript 2.3, we can optionally add a default type to each of our generic type parameters. In our case, this allows us to specify that both Props and State should be the any type if no type argument is given explicitly:

declare namespace React {
  class Component<Props = any, State = any> {
    props: Props;
    state: State;
  }
}

Now, our initial code example type-checks and compiles successfully again with both Props and State typed as any:

class Greeting extends React.Component {
  render() {
    return <span>Hello, {this.props.name}!</span>;
  }
}

Of course, we can still explicitly provide a type for the Props type parameter and override the default any type, just as we did before:

type GreetingProps = { name: string };

class Greeting extends React.Component<GreetingProps, any> {
  render() {
    return <span>Hello, {this.props.name}!</span>;
  }
}

We can do other interesting things as well. Both type parameters now have a default type, which makes them optional — we don't have to provide them! This allows us to specify an explicit type argument for Props while implicitly falling back to any for the State type:

type GreetingProps = { name: string };

class Greeting extends React.Component<GreetingProps> {
  render() {
    return <span>Hello, {this.props.name}!</span>;
  }
}

Note that we're only providing a single type argument. We can only leave out optional type arguments from the right, though. That is, it's not possible in this case to specify a type argument for State while falling back to the default Props type. Similarly, when defining a type, optional type parameters must not be followed by required type parameters.

#Another Example

In my previous post about mixin classes in TypeScript 2.2, I originally declared the following two type aliases:

type Constructor<T> = new (...args: any[]) => T;
type Constructable = Constructor<{}>;

The Constructable type is purely syntactic sugar. It can be used instead of the Constructor<{}> type so that we don't have to write out the generic type argument each time. With generic parameter defaults, we could get rid of the additional Constructable type altogether and make {} the default type:

type Constructor<T = {}> = new (...args: any[]) => T;

The syntax is slightly more involved, but the resulting code is cleaner. Nice!

This article and 44 others are part of the TypeScript Evolution series. Have a look!