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:
Rendering our component yields the following HTML, as we would expect:
#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 ComponentProps, 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.ComponentGreetingProps, any> render() return span>Hello, this.props.name>!span>; > >
- 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:
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' // 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 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 ComponentProps = 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.ComponentGreetingProps, 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.ComponentGreetingProps> 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 ConstructorT> = 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 ConstructorT = <>> = 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!
Advanced TypeScript Fundamentals
A deep dive into the fundamnetals of TypeScript’s type system. Learn about the optional chaining ( ?. ) and nullish coalescing ( ?? ) operators, assertion functions, truly private class fields, conditional types, template literal types, adn more.
Implementing generic parameter defaults
In this lesson, we will learn how to define a default on a generic parameter.
Generic parameter default type syntax
A default type can be defined on a generic parameter as follows:
We specify the default type after the equals ( = ) sign after the generic parameter.
Generic parameter defaults can be added to functions, interfaces, type aliases, and classes.
We are going to explore default generic parameters in an exercise.
The code contains a generic function and a generic interface. We are going to explore the generic interface first.
const button: Component = name: "Button", props: text: "Save", >, log: () => console.log("Save button"), >;
This creates an object with the Component interface.
A type error occurs on the button variable. Why is this so?
We could of course pass in the types as follows:
const button: Componentstring, text: string >> = .
. but we aren’t going to do that!
The type errors have been resolved now.
1console.log(button.props.text); 2 console.log(button.props.text2);
Why isn’t a type error raised on line 2?
When a generic parameter default is used, the type might be wider than necessary.
Let’s move on to explore the generic function.
const first = firstOrNull([1, 2, 3]); console.log(first);
We haven’t supplied the generic parameter to the function. So, why doesn’t a type error occur?
What is the type of the first variable? This will be inferred from the return type of firstOrNull .
Default types for generic parameters can make interfaces, type aliases, and classes easier to consume. It is important to be aware that consuming code can be weaker typed if this approach is used.
Generic functions don’t require generic parameters to be passed. TypeScript can generally infer generic parameter types on functions from the parameters passed into it. So, generic parameter defaults aren’t that useful on generic functions.
In the next lesson, we will learn how to constrain the type of generic parameters.