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.
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.
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:
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.
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.
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; };
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[]
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.
More about the specific arguments can be found in the package documentation.