e
Working with an existing codebase
Working with an existing codebase
When you dive into an existing codebase for the first time, you should seek an overview of its conventions and structures. You can start this research by reading the README.md in the root of the repository. Usually, the README contains a brief description of the application and the requirements for using it, as well as how to start it for development. If the README is not available or poorly written, you can take a peek at the package.json. You also should start the application and click around to verify you have a functional development environment.
You can also browse the folder structure to get some insight into the application's functionality and/or the architecture used. These are not always clear, and the developers might have chosen a way to organize code that is not familiar to you. The patientia frontend you cloned previously will be used in the rest of this part and is organized, feature-wise. You can see what pages the application has, and some general components, e.g. modals and state. Keep in mind that the features may have different scopes. For example, modals are visible UI-level components whereas the state is comparable to business logic and keeps the data organized under the hood for the rest of the app to use.
TypeScript provides types for what kind of data structures, functions, components, and state to expect. You can try looking for types.ts or something similar to get started. IDEs can be a big help and simply highlighting variables and parameters can provide quite a lot of insight. All this naturally depends on how types are used in the project.
If the project has unit, integration or end-to-end tests, reading those is most likely beneficial. Test cases are your most important tool when refactoring or adding new features to the application. You want to make sure not to break any existing features when hammering around the code. TypeScript can also give you guidance with argument and return types when changing the code.
Remember that reading code is a skill in itself, so don't worry if you don't understand the code on your first readthrough. The code may have a lot of corner cases, and pieces of logic may have been added here and there throughout its development cycle. It is hard to imagine what kind of problems the previous developer has wrestled with. Think of it all like growth rings in trees. Understanding everything requires digging deep into the code and business domain requirements. The more code you read, the better you will be at understanding it. You will most likely read far more code than you are going to produce throughout your life.
Patientia frontend
It's time to get our hands dirty finalizing the frontend for the backend we built in exercises 8.8.-8.13. We will also add some new features to the backend for finishing the app.
Before diving into the code, let's start both the frontend and the backend.
If all goes well, you should see a patient listing page. It fetches a list of patients from our backend, and renders it to the screen as a simple table. There is also a button for creating new patients on the backend. As we are using mock data instead of a database, the data will not persist - closing the backend will delete all the data we have added. UI design has not been a strong point of the creators, so let's disregard the UI for now.
After verifying that everything works, we can start studying the code. All of the interesting stuff resides in the src folder. For your convenience, there is already a types.ts file for basic types used in the app, which you will have to extend or refactor in the exercises.
In principle, we could use the same types for both backend and frontend, but usually, the frontend has different data structures and use cases for the data, which causes the types to be different. For example, the frontend has a state and may want to keep data in objects or maps whereas the backend uses an array. The frontend might also not need all the fields of a data object saved in the backend, and it may need to add some new fields to use for rendering.
The folder structure looks as follows:

Besides the component App
, there are currently three main components:
AddPatientModal
and PatientListPage
which are both defined in a directory, and a component HealthRatingBar
defined in a file.
If a component has some subcomponents not used elsewhere in the app, some suggest defining that component and its subcomponents in a directory.
For example, the src/AddPatientModal folder currently houses two components:
AddPatientModal
defined in index.tsxAddPatientForm
a subcomponent ofAddPatientModal
defined in AddPatientForm.tsx.
There is nothing very surprising in the code.
The state and communication with the backend are implemented with the useState
hook and Axios, similar to the tasks app in the previous section.
Material UI is used to style the app and the navigation structure is implemented with
React Router,
both familiar to us from part 7 of the course.
From the typing point of view, there are a couple of interesting things.
Component App
passes the function setPatients
as a prop to the component PatientListPage
:
const App = () => {
const [patients, setPatients] = useState<Patient[]>([]);
// ...
return (
<div className="App">
<Router>
<Container>
<Routes>
// ...
<Route path="/" element={
<PatientListPage
patients={patients}
setPatients={setPatients} />}
/>
</Routes>
</Container>
</Router>
</div>
);
};
To keep the TypeScript compiler happy, the props are typed as follows in src/PatientListPage/index.tsx:
interface Props {
patients : Patient[]
setPatients: React.Dispatch<React.SetStateAction<Patient[]>>
}
const PatientListPage = ({ patients, setPatients } : Props ) => {
// ...
}
So the function setPatients
has type React.Dispatch<React.SetStateAction<Patient[]>>.
We can see the type in the editor when we hover over the function:

The React TypeScript cheatsheet's code blocks for
AppProps
have some nice lists for typical prop types. Use that cheatsheet later to help find the correct types for props that are not obvious.
PatientListPage
passes four props to the component AddPatientModal
Two of these props are functions.
Here's the relevant code.
const PatientListPage = ({ patients, setPatients } : Props ) => {
const [modalOpen, setModalOpen] = useState<boolean>(false);
const [error, setError] = useState<string>();
// ...
const closeModal = (): void => { setModalOpen(false);
setError(undefined);
};
const submitNewPatient = async (values: PatientFormValues) => { // ...
};
// ...
return (
<div className="App">
// ...
<AddPatientModal
modalOpen={modalOpen}
onSubmit={submitNewPatient} error={error}
onClose={closeModal} />
</div>
);
};
Let us have a look how these are typed. The types, in AddPatientModal/index.tsx look like this:
interface Props {
modalOpen: boolean;
onClose: () => void;
onSubmit: (values: PatientFormValues) => Promise<void>;
error?: string;
}
const AddPatientModal = ({ modalOpen, onClose, onSubmit, error }: Props) => {
// ...
}
onClose
is just a function that takes no parameters, and does not return anything, so the type is:
() => void
The type of onSubmit
is a bit more interesting, it has one parameter that has the type PatientFormValues
.
The return value of the function is Promise<void>
.
So again the function type is written with the arrow syntax:
(values: PatientFormValues) => Promise<void>
The return value of an async
function is a
promise
with the value that the function returns.
Our function does not return anything so the correct return type is just Promise<void>
.
Full entries
In exercise 8.10
we implemented an endpoint for fetching information about various diagnoses, but we are still not using that endpoint at all.
Since we now have a page for viewing a patient's information, it would be nice to expand our data a bit.
Let's add an Entry
field to our patient data so that a patient's data contains their medical entries, including possible diagnoses.
Let's ditch our old patient seed data from the backend and start using this expanded format.
Let's also start fleshing out our empty Entry
in types.ts based on the data we have.
If we take a closer look at the data, we can see that the entries are quite different from one another. For example, let's take a look at the first two entries:
{
id: 'd811e46d-70b3-4d90-b090-4535c7cf8fb1',
date: '2015-01-02',
type: 'Hospital',
specialist: 'Eggman',
diagnosisCodes: ['S62.5'],
description:
"Healing time appr. 2 weeks. patient doesn't remember how he got the injury.",
discharge: {
date: '2015-01-16',
criteria: 'Hand has healed.',
}
}
...
{
id: 'fcd59fa6-c4b4-4fec-ac4d-df4fe1f85f62',
date: '2019-08-05',
type: 'OccupationalHealthcare',
specialist: 'Eggman',
employerName: 'SNPP',
diagnosisCodes: ['Z57.1', 'Z74.3', 'M51.2'],
description:
'Patient mistakenly found himself in a nuclear plant waste site without protection gear. Very minor radiation poisoning. ',
sickLeave: {
startDate: '2019-08-05',
endDate: '2019-08-28'
}
}
Immediately, we can see that while the first few fields are the same, the first entry has a discharge
field and the second entry has employerName
and sickLeave
fields.
All the entries seem to have some fields in common, but some fields are entry-specific.
When looking at the type
, we can see that there are three kinds of entries:
OccupationalHealthcare
Hospital
HealthCheck
This indicates we need three separate types. Since they all have some fields in common, we might just want to create a base entry interface that we can extend with the different fields in each type.
When looking at the data, it seems that the fields id
, description
, date
and specialist
are something that can be found in each entry.
On top of that, it seems that diagnosisCodes
is only found in one OccupationalHealthcare
and one Hospital
type entry.
Since it is not always used even in those types of entries, it is safe to assume that the field is optional.
We could consider adding it to the HealthCheck
type as well
since it might just not be used in these specific entries.
So our BaseEntry
from which each type could be extended would be the following:
interface BaseEntry {
id: string;
description: string;
date: string;
specialist: string;
diagnosisCodes?: string[];
}
If we want to finetune it a bit further, since we already have a Diagnosis
type defined in the backend,
we may want to refer to the code field of the Diagnosis
type directly in case its type ever changes.
We can do that like so:
interface BaseEntry {
id: string;
description: string;
date: string;
specialist: string;
diagnosisCodes?: Diagnosis['code'][];
}
We could define an array with the syntax Array<Type>
instead of defining it Type[]
(as mentioned earlier.
In this particular case, writing Diagnosis['code'][]
starts to look a bit strange so we will decide to use the alternative syntax
(that is also recommended by the ESlint rule array-simple):
interface BaseEntry {
id: string;
description: string;
date: string;
specialist: string;
diagnosisCodes?: Array<Diagnosis['code']>;}
Now that we have the BaseEntry
defined in types.ts, we can start creating the extended entry types we will actually be using.
Let's start by creating the HealthCheckEntry
type.
Entries of type HealthCheck
contain the field HealthCheckRating
, which is an integer from 0 to 3, zero meaning Healthy
and 3 meaning CriticalRisk
.
This is a perfect case for an enum definition.
With these specifications we could write a HealthCheckEntry
type definition like so:
export enum HealthCheckRating {
"Healthy" = 0,
"LowRisk" = 1,
"HighRisk" = 2,
"CriticalRisk" = 3
}
interface HealthCheckEntry extends BaseEntry {
type: "HealthCheck";
healthCheckRating: HealthCheckRating;
}
Now we only need to create the OccupationalHealthcareEntry
and HospitalEntry
types so we can combine them in a union and export them as an Entry type like this:
export type Entry =
| HospitalEntry
| OccupationalHealthcareEntry
| HealthCheckEntry;
Omit with unions
An important point concerning unions is that, when you use them with Omit
to exclude a property, it works in a possibly unexpected way.
Suppose we want to remove the id
from each Entry
.
We could think of using
Omit<Entry, 'id'>
but it wouldn't work as we might expect.
In fact, the *resulting type would only contain the common properties, but not the ones they don't share*.
A possible workaround is to define a special Omit
-like function to deal with such situations:
// Define special omit for unions
type UnionOmit<T, K extends string | number | symbol> = T extends unknown ? Omit<T, K> : never;
// Define Entry without the 'id' property
type EntryWithoutId = UnionOmit<Entry, 'id'>;