Introduction
These notes should help in better understanding TypeScript
and might be helpful when needing to lookup up how leverage TypeScript in a specific situation. All examples are based on TypeScript 3.2.
Generics
If you have been reading along the "Notes on TypeScript" series, then you will have seen extensive usage of generics so far. While we have been using generics, we haven't actually talked about generics and why they are useful. In this part of the series, we will first try to better understand the generics topics and then see how we can leverage generics when working with React and TypeScript.
When writing software, one aspect is that we want to be able to reuse some functionality without having to write a specific functionality for every possible input type. Let's take the following example as a starting point:
function isDefinedNumber(a: number) : boolean {
return a !== null || a !== undefined;
}
function isDefinedString(a: string) : boolean {
return a!== null || a !== undefined;
}
We wouldn't write explicit functions for string
or number
inputs, rather we would write a function with the following signatures:
function isDefined<Type>(a: Type) : boolean {
return a!== null || a !== undefined;
}
isDefined
expects an input of generic Type
. TypeScript will try to infer the argument and assign the correct type. Let's continue with another example, where we want to infer the return type:
function of<Type>(a: Type) : Type[] {
return [a];
}
const toNumbers = of(1); // const toNumbers: number[]
const toStrings = of("Test Of"); // const toString: string[]
In the of
example, we can see that we don't even need to define the type, as TypeScript can infer the argument type. This is not applicable in all cases, sometimes we have to be explicit about the type. We could also have defined the above functions like so:
const toNumbers = of<number>(1); // const toNumbers: number[]
const toStrings = of<string>("Test Of"); // const toString: string[]
Technically we could have used any
:
function of(a: any) : any {
if (a.length !== undefined) {
return a
}
return a;
}
But there is a big difference between using any
and generics. If you take a closer look at the above example, we don't know anything about the input argument. Calling of
with an undefined
or null
value will result in an error. Generics can infer the exact type and enforce to handle the input accordingly inside the function body. The same example using generics:
function of<Type>(a: Type) : Type[] {
if (a.length !== undefined) { // error: Property 'length' does not exist on 'Type'
return a
}
return [a];
}
We have to be more explicit when dealing with generics, the example can be rewritten to the following:
function of<Type>(a: Type | Type[]) : Type[] {
if (Array.isArray(a)) {
return a
}
return [a];
}
const a = of(1); // const a: number[]
const b = of([1]); // const b: number[]
Using generics enables us to reuse functionality, as a
is of type Type
or an array of type Type
. When passing in 1
as an argument, Type
binds to number
, the same happens when passing in [1]
, Type
binds to number
.
While we have seen functions using generics, we can also use generics with classes, which might be interesting when writing class components in React.
class GenericClass<Type> {
of = (a: Type | Type[]): Type[] => {
if (Array.isArray(a)) {
return a;
}
return [a];
};
}
const genericClass = new GenericClass<number>();
const a = genericClass.of(1); // const a: number[]
const b = genericClass.of("1"); // error!
const c = genericClass.of([1]); // const c: number[]
The examples we have seen so far should help us in understanding the basics, we will build on this knowledge when using generics with React components.
React and Generics
When working with React we might have a function component where we need to infer the argument type.
We might be building a component that expects a number or string or an array of type number or string.
type RowProps<Type> = {
input: Type | Type[];
};
function Rows<Type>({input}: RowProps<Type>) {
if (Array.isArray(input)) {
return <div>{input.map((i, idx) => <div key={idx}>{i}</div>)}</div>
}
return <div>{input}</div>
}
// usage
<Rows input={[1]} />
<Rows input={1} />
<Rows input={true} /> // Also works!
This works, but it also works for any value right now. We can pass in true
and TypeScript will not complain. We need to restrict Type
by ensuring Type
either extends number
or string
.
function Rows<Type extends number | string>({input}: RowProps<Type>) {
if (Array.isArray(input)) {
return <div>{input.map((i, idx) => <div key={idx}>{i}</div>)}</div>
}
return <div>{input}</div>
}
<Rows input={[1]} />
<Rows input={1} />
<Rows input="1" />
<Rows input={["1"]} />
<Rows input={true} /> //Error!
We can ensure that only expected types can be provided now. It's also interesting to note that we can make our prop type definition generic, as seen in the above example:
type RowProps<Type> = {
input: Type | Type[];
};
Next, we will build a more advanced example to see why generics can help us build reusable React components. We will build a component that expects two different inputs. Based on these inputs we will calculate a third value and the pass in a flat object based on the original inputs as well as the newly calculated value to a provided render prop.
type RenderPropType<InputType, OtherInputType> = { c: number } & InputType &
OtherInputType;
type RowComponentPropTypes<InputType, OtherInputType> = {
input: InputType;
otherInput: OtherInputType;
render: (props: RenderPropType<InputType, OtherInputType>) => JSX.Element;
};
The first step is to define the RowComponentPropTypes
, where we let TypeScript infer the provided arguments, and based on the bind types define the render
function via using RenderPropType
. RenderPropType
is an intersection of the new type {c: number}
, which we will calculate, and InputType
and OtherInputType
. We have been making heavy use of generics so far.
We might not know the exact shape of the provided inputs, so our next step is to restrict the provided types on the component level.
class RowComponent<
InputType extends { a: number },
OtherInputType extends { b: number }
> extends React.Component<RowComponentPropTypes<InputType, OtherInputType>> {
// implementation...
}
By using InputType extends { a: number }
we can ensure that our input has an a
property of type number
provided, the same for OtherInputType
. Now we can implement the RowComponent
that ensures we can provide a, b, c
properties to a render
function.
Finally, this is our complete example implementation:
class RowComponent<
InputType extends { a: number },
OtherInputType extends { b: number }
> extends React.Component<RowComponentPropTypes<InputType, OtherInputType>> {
convert = (input: InputType, output: OtherInputType) => {
return { c: input.a + output.b, ...input, ...output };
};
render() {
return this.props.render(
this.convert(this.props.input, this.props.otherInput)
);
}
}
<RowComponent
input={{ a: 1 }}
otherInput={{ b: 2 }}
render={({ a, b, c }) => (
<div>
{a} {b} {c}
</div>
)}
/>
We should have a basic understanding of generics and how to leverage them when working with React and TypeScript now.
If you have any questions or feedback please leave a comment here or connect via Twitter: A. Sharif
Top comments (2)
Would suggest to change
JSX.Element
toReact.ReactNode
since it's less magical and it can be future safe.This was so useful! Thanks!!