Real-world types with objects
Chapter 2: Getting started with TypeScript
Chapters - Table of contents
Typing variables is great; However, most real-world applications rely on more complex data types such as objects.
For example, an application's UI-Kit could provide the following <Button /> props:
1const buttonProps = {
2  name: 'save',
3  label: 'Save',
4  size: 'medium',
5  type: 'primary',
6  classNames: ['mt-50', 'mb-50']
7}
8
Let's see how to type objects in TypeScript with the help of the interface keyword.

Describe objects with interface

Typing object is close to writing objects in JavaScript:
1interface ButtonProps {
2  name: string
3}
4
5const buttonProps: ButtonProps = { name: "save" }
6
Our button variable is of our custom type Button that requires a name property.
Unlike variables, interface properties types don’t include null and undefined by default
Let's now add all the properties exposed in our initial buttonProps object:
1interface ButtonProps {
2  name: string,
3  type: 'primary' | 'secondary',
4  size: 'small' | 'medium' | 'large',
5  label: string,
6  classNames: string[]
7}
8
9const buttonProps: ButtonProps = {
10  name: 'save',
11  label: 'Save',
12  size: 'medium',
13  type: 'primary',
14  classNames: ['mt-50', 'mb-50']
15}
16
As you can see, typing object properties is similar to typing variables.

Optional properties

Most buttons would have a default label; For this reason, we need the label property to be optional. We can achieve this by prefixing the type definition with a ? as it follows:
1interface ButtonProps {
2  name: string,
3  type: 'primary' | 'secondary',
4  size: 'small' | 'medium' | 'large',
5  // equivalent to `string | undefined`
6  label?: string,
7  classNames: string[]
8}
9
The ? is not exactly equivalent to adding | undefined. The ? prefix indicates that a property is optional, see the difference below:
1interface Animal {
2  name: string | undefined
3}
4
5// Property 'name' is missing in type '{}' but required in type 'Animal'.ts(2741)
6const cat: Animal = {}
7
8
9interface Animal {
10  name?: string
11}
12
13// Valid
14const cat: Animal = {}
15
16

Immutable properties

Also, we want to ensure that any buttons' names cannot be changed. We can achieve this behavior by leveraging the readonly keyword:
1interface ButtonProps {
2  readonly name: string,
3  type: 'primary' | 'secondary',
4  size: 'small' | 'medium' | 'large',
5  // equivalent to `string | undefined`
6  label?: string,
7  classNames: string[]
8}
9
10
Remember that, like all TypeScript types, the readonly keyword does not guarantee immutability at runtime.

interface types are closed by default

Unlike object types in Flow, TypeScript interface types are "closed by default," meaning that you cannot add properties not defined by its type:
1interface ButtonProps {
2  name: string,
3  type: 'primary' | 'secondary',
4  size: 'small' | 'medium' | 'large',
5  label: string,
6  classNames: string[]
7}
8
9const buttonProps: ButtonProps = {
10  name: 'save',
11  label: 'Save',
12  size: 'medium',
13  type: 'primary',
14  classNames: ['mt-50', 'mb-50']
15}
16
17// ERROR: "Property 'myCustomProp' 
18//   does not exist on type 'ButtonProps'.ts(2339)"
19buttonProps.myCustomProp = { custom: 'data' }
20
ℹ️ We will see in Chapter 3 how to build "open object types" by leveraging indexes signatures.

Compose and extend interfaces

On top of the Button component, our UI-Kit could also provide a Checkbox component, defined as it follows:
1interface CheckboxProps {
2  name: string,
3  type: 'primary' | 'secondary',
4  size: 'small' | 'medium' | 'large',
5  label: string,
6  classNames: string[]
7  value: boolean
8}
9
We can see that CheckboxProps shares a lot of common properties with ButtonProps. Those properties, mostly theme-related (size, type) or form related (label) will be found in most components exposed by the UI-kit. For this reason, and to avoid any divergence between components, we should extract those common properties in dedicated types as follows:
1interface ThemableComponentProps {
2  type: 'primary' | 'secondary',
3  size: 'small' | 'medium' | 'large'
4  classNames: string[]
5}
6
7interface FormComponentProps {
8  name: string,
9  label?: string,
10  disabled?: boolean,
11}
12
Now we can update the ButtonProps and CheckboxProps definitions to use the common ThemableComponentProps and FormComponentProps types using the extends keyword as follows:
1interface CheckboxProps extends ThemableComponentProps, FormComponentProps {
2  value: boolean
3}
4
5interface ButtonProps extends ThemableComponentProps, FormComponentProps {
6  loading?: boolean // adding a loading state to `<Button />`
7}
8
Leveraging extends allows us to build:
  • easier to maintain and reusable types
  • safer types (avoid divergence in common types definition)
ℹ️ We will see in Chapter 3 how to build more advanced reusable types patterns

Side note: beware of the inference of objects

Again, the question "Do we need to type all objects?" resurface.
Considering that, in real-world applications, most objects are dynamic (not constant), I would suggest typing all of them to provide a flexible and rich developer experience.
Let's take a look at some examples of type inference on objects:
1const buttonProps = {
2  width: 100,
3  label: 'Continue',
4  color: 'blue'
5}
6
7// the inferred type of buttonProps would be:
8//    {
9//        width: string;
10//        label: number;
11//        color: string;
12//    }
13
14const constButtonProps = {
15  width: 100,
16  label: 'Continue',
17  color: 'blue'
18} as const
19
20// the inferred type of constButtonProps would be:
21//    {
22//        readonly width: 100;
23//        readonly label: "Continue";
24//        readonly color: "blue";
25//    }
26
We can see that both type inferences scenarios are not satisfying:
  • The first infers a typing that is "too open": string typed properties everywhere.
  • The second is inferring literal types for all properties, which are too specifics.

Moving forward

We saw how to type objects with the example of component props. In the next part, we will see how to link those props types to the components.
We use cookies to collect statistics through Google Analytics.
Do not track
 
Allow cookies