Generate TypeScript types from Django REST framework serializers

Our Blog

Lukáš Letovanec12.07.202115 minutes

The following article introduces our Django package that is useful for improving the development experience when working with Django REST framework and frontend application which is using TypeScript. Continue reading and learn more about it.

The 'Full stack' problem

In modern web development, there is a trend to create reactive, fast websites which offer an amazing experience for the users. A widely used pattern to achieve this in Django projects is to have a frontend application written in Javascript framework or library - there are quite a few to choose from - and connect it to the backend using Django REST framework (DRF).

To improve the development experience and produce high-quality code developers often opt to use TypeScript instead of Javascript. It's important to know the format of the data which some endpoint expects or returns - of course for this you can just look at the API documentation, but there is a huge improvement in the development experience when you have these expected responses or requests typed in your frontend application. It improves correct code completion and linting, which eliminates runtime errors in a significant way.

Since TypeScript and Django Serializers cannot share the definition of the format of the API data, it can get annoying to change it both in backend and frontend every time there is a change made to the endpoint - especially when you are a full stack developer working on both ends. Or even type it on the frontend correctly in the first place. Now there is an easy way to do it with drf-typescript-generator package.

How it works

The package provides a management command for generating TypeScript types from serializers found in the Django apps you pass to it as arguments. You can also choose to generate interfaces instead if it fits more to your coding style.

The process of finding serializers follows general Django practices, but for example names of files in which serializers should be put is not strictly defined. Someone uses serializers.py, someone api.py and someone puts serializers directly into views.py. It's personal preference and the package takes it into account. With that said, the process of finding serializers can be divided into the following steps:

  1. The process starts with the DRF routers which are found in the project urls.py file as well as urls.py files of chosen apps.
  2. Registered Viewsets are extracted from the routers and used to identify modules that contain them.
  3. All serializers found in those modules (serializers are either defined in them or imported from other modules) are considered as being relevant and are then exported.

This approach prevents generating output for unnecessary apps and serializers that you do not need in your frontend application and at the same time, you don't need to make changes in your backend code to mark relevant serializers.

Generating TypeScript

Classes in Python, as well as types in TypeScript, use a naming convention known as PascalCase, so in the generated output the name of the serializer stays also as the name of the type. For the properties Python usually uses snake_case and TypeScript camelCase therefore the names of serializer fields are transformed - e.g. house_number will become houseNumber. Unfortunately, Django REST framework does not accept camel case in request data out of the box so you would need to map the fields to snake case before sending a request or use a package such as djangorestframework-camel-case to provide this functionality at the backend.

Serializers to types

In addition to the classic DRF Serializer which is commonly used, the package also works well with ModelSerializer and with nested serializers. In the case of nested serializers, the TypeScript equivalent is generated even when the nested serializer referred to in the main serializer is not directly used in views modules and so the output is without missing type definitions. Example of the nested serializer and generated TS types:

class CompanyInfo(serializers.Serializer): name = serializers.CharField(max_length=100) class BillingInfo(serializers.Serializer): company = CompanyInfo()
export type CompanyInfo = { name: string; }; export type BillingInfo = { company: CompanyInfo; };

Fields to properties

A lot of various types of fields are already supported. Alongside simple ones like CharField, IntegerField, BooleanField, etc. there are also more complicated like ListField, SerializerMethodField, MultipleChoiceField. Type hinting is taken into account when deriving property type for SerializerMethodField as can be seen in the following example:

class Foo(serializers.Serializer): result = serializers.SerializerMethodField() result_unknown = serializers.SerializerMethodField() # method with type hint def get_result(self, obj) -> int: return 42 # method without type hint def get_result_unknown(self, obj): return None
export type Foo = { result: number; resultUnknown: any; };

Serializer field keyword arguments are also taken into account. Optional fields are marked accordingly in TypeScript code:

count = serializers.IntegerField(required=False)
count?: number

Sometimes we want the field to stay mandatory (must be included in the payload) but to allow null values. For that, we can set allow_null=True to the field resulting in a composite TS type similar to the one generated by ChoiceField:

choices = serializers.ChoiceField(choices=[1, 2, 3]) count = serializers.IntegerField(allow_null=True)
choices: 1 | 2 | 3; count: number | null;

Array-like fields results in Array TypeScript type with correct child type derived:

complex_numbers = ComplexNumber(many=True) # this is example nested serializer limited_numbers = serializers.MultipleChoiceField(choices=[1, 2, 3]) numbers = serializers.ListField(child=serializers.IntegerField())
complexNumbers: ComplexNumber[] limitedNumbers: (1 | 2 | 3)[] numbers: number[]

Output format

Code style differs among companies and individual developers and with that in mind command allows you to configure the style of generated TS code using different arguments. You can choose whether you want to have serializers generated as types or as interfaces if you want to have semicolons in the output and of course... to choose between tabs or spaces for indentation.

Tabs vs Spaces {1200x1200}

More about the specific arguments can be found in the package documentation.

© 2024 Created by Remaster. All rights reserved.

Company ID: CZ10666648