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 parameterProps
- Similarly,
any
is the type argument for the type parameterState
With these types in place, we now get better type checking and autocompletion suggestions within our 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!