Skip to content

Commit 575138f

Browse files
Merge pull request #804 from open-formulieren/chore/445-convert-to-typescript
Convert some low-hanging fruit to TypeScript
2 parents 19fc530 + 7dc4a84 commit 575138f

20 files changed

+228
-151
lines changed

.prettierrc.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"tabWidth": 2,
1616
"trailingComma": "es5",
1717
"useTabs": false,
18-
"importOrder": ["^((api-mocks|components|data|formio|hooks|map|routes|story-utils|types)/(.*)|(api|api-mocks|cache|Context|errors|headers|i18n|routes|sdk|sentry|types|utils))$", "^[./]"],
18+
"importOrder": ["^((api-mocks|components|data|formio|hooks|map|routes|story-utils|types)/(.*)|(api|api-mocks|cache|Context|errors|headers|i18n|routes|sdk|sentry|types|utils))$", "^@/.*", "^[./]"],
1919
"importOrderSeparation": true,
2020
"importOrderSortSpecifiers": true
2121
}

package-lock.json

+21-26
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"@open-formulieren/formiojs": "^4.13.14",
3636
"@open-formulieren/leaflet-tools": "^1.0.0",
3737
"@sentry/react": "^8.50.0",
38-
"classnames": "^2.3.1",
38+
"clsx": "^2.1.1",
3939
"date-fns": "^4.1.0",
4040
"flatpickr": "^4.6.9",
4141
"formik": "^2.2.9",

src/api.js src/api.ts

+103-28
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,29 @@ import {
1212
import {CSPNonce, CSRFToken, ContentLanguage, IsFormDesigner} from './headers';
1313
import {setLanguage} from './i18n';
1414

15-
const fetchDefaults = {
16-
credentials: 'include', // required for Firefox 60, which is used in werkplekken
15+
interface ApiCallOptions extends Omit<RequestInit, 'headers'> {
16+
headers?: Record<string, string>;
17+
}
18+
19+
const fetchDefaults: ApiCallOptions = {
20+
credentials: 'include',
1721
};
1822

1923
const SessionExpiresInHeader = 'X-Session-Expires-In';
2024

21-
let sessionExpiresAt = createState({expiry: null});
25+
interface SessionExpiryState {
26+
expiry: Date | null;
27+
}
28+
29+
const sessionExpiresAt = createState<SessionExpiryState>({expiry: null});
2230

23-
export const updateSessionExpiry = seconds => {
31+
export const updateSessionExpiry = (seconds: number): void => {
2432
const newExpiry = new Date();
2533
newExpiry.setSeconds(newExpiry.getSeconds() + seconds);
2634
sessionExpiresAt.setValue({expiry: newExpiry});
2735
};
2836

29-
const throwForStatus = async response => {
37+
const throwForStatus = async (response: Response): Promise<void> => {
3038
if (response.ok) return;
3139

3240
let responseData = null;
@@ -75,7 +83,10 @@ const throwForStatus = async response => {
7583
throw new ErrorClass(errorMessage, response.status, responseData.detail, responseData.code);
7684
};
7785

78-
const addHeaders = (headers, method) => {
86+
const addHeaders = (
87+
headers: Record<string, string> | undefined,
88+
method: string
89+
): Record<string, string> => {
7990
if (!headers) headers = {};
8091

8192
// add the CSP nonce request header in case the backend needs to do any post-processing
@@ -94,10 +105,10 @@ const addHeaders = (headers, method) => {
94105
return headers;
95106
};
96107

97-
const updateStoredHeadersValues = headers => {
108+
const updateStoredHeadersValues = (headers: Headers): void => {
98109
const sessionExpiry = headers.get(SessionExpiresInHeader);
99110
if (sessionExpiry) {
100-
updateSessionExpiry(parseInt(sessionExpiry), 10);
111+
updateSessionExpiry(parseInt(sessionExpiry, 10));
101112
}
102113

103114
const CSRFTokenValue = headers.get(CSRFToken.headerName);
@@ -117,7 +128,7 @@ const updateStoredHeadersValues = headers => {
117128
}
118129
};
119130

120-
const apiCall = async (url, opts = {}) => {
131+
const apiCall = async (url: string, opts: ApiCallOptions = {}): Promise<Response> => {
121132
const method = opts.method || 'GET';
122133
const options = {...fetchDefaults, ...opts};
123134
options.headers = addHeaders(options.headers, method);
@@ -129,7 +140,17 @@ const apiCall = async (url, opts = {}) => {
129140
return response;
130141
};
131142

132-
const get = async (url, params = {}, multiParams = []) => {
143+
/**
144+
* Make a GET api call to `url`, with optional query string parameters.
145+
*
146+
* The return data is the JSON response body, or `null` if there is no content. Specify
147+
* the generic type parameter `T` to get typed return data.
148+
*/
149+
const get = async <T = unknown>(
150+
url: string,
151+
params: Record<string, string> = {},
152+
multiParams: Record<string, string>[] = []
153+
): Promise<T | null> => {
133154
let searchParams = new URLSearchParams();
134155
if (Object.keys(params).length) {
135156
searchParams = new URLSearchParams(params);
@@ -142,16 +163,43 @@ const get = async (url, params = {}, multiParams = []) => {
142163
}
143164
url += `?${searchParams}`;
144165
const response = await apiCall(url);
145-
const data = response.status === 204 ? null : await response.json();
166+
const data: T | null = response.status === 204 ? null : await response.json();
146167
return data;
147168
};
148169

149-
const _unsafe = async (method = 'POST', url, data, signal) => {
150-
const opts = {
170+
export interface UnsafeResponseData<T = unknown> {
171+
/**
172+
* The parsed response body JSON, if there was one.
173+
*/
174+
data: T | null;
175+
/**
176+
* Whether the request completed successfully or not.
177+
*/
178+
ok: boolean;
179+
/**
180+
* The HTTP response status code.
181+
*/
182+
status: number;
183+
}
184+
185+
/**
186+
* Make an unsafe (POST, PUT, PATCH) API call to `url`.
187+
*
188+
* The return data is the JSON response body, or `null` if there is no content. Specify
189+
* the generic type parameter `T` to get typed return data, and `U` for strongly typing
190+
* the request data (before JSON serialization).
191+
*/
192+
const _unsafe = async <T = unknown, U = unknown>(
193+
method = 'POST',
194+
url: string,
195+
data: U,
196+
signal?: AbortSignal
197+
): Promise<UnsafeResponseData<T>> => {
198+
const opts: ApiCallOptions = {
151199
method,
152200
headers: {
153201
'Content-Type': 'application/json',
154-
[CSRFToken.headerName]: CSRFToken.getValue(),
202+
[CSRFToken.headerName]: CSRFToken.getValue() ?? '',
155203
},
156204
};
157205
if (data) {
@@ -161,30 +209,57 @@ const _unsafe = async (method = 'POST', url, data, signal) => {
161209
opts.signal = signal;
162210
}
163211
const response = await apiCall(url, opts);
164-
const responseData = response.status === 204 ? null : await response.json();
212+
const responseData: T | null = response.status === 204 ? null : await response.json();
165213
return {
166214
ok: response.ok,
167215
status: response.status,
168216
data: responseData,
169217
};
170218
};
171219

172-
const post = async (url, data, signal) => {
173-
const resp = await _unsafe('POST', url, data, signal);
174-
return resp;
175-
};
220+
/**
221+
* Make a POST call to `url`.
222+
*
223+
* The return data is the JSON response body, or `null` if there is no content. Specify
224+
* the generic type parameter `T` to get typed return data, and `U` for strongly typing
225+
* the request data (before JSON serialization).
226+
*/
227+
const post = async <T = unknown, U = unknown>(
228+
url: string,
229+
data: U,
230+
signal?: AbortSignal
231+
): Promise<UnsafeResponseData<T>> => await _unsafe<T, U>('POST', url, data, signal);
176232

177-
const patch = async (url, data = {}) => {
178-
const resp = await _unsafe('PATCH', url, data);
179-
return resp;
180-
};
233+
/**
234+
* Make a PATCH call to `url`.
235+
*
236+
* The return data is the JSON response body, or `null` if there is no content. Specify
237+
* the generic type parameter `T` to get typed return data, and `U` for strongly typing
238+
* the request data (before JSON serialization).
239+
*/
240+
const patch = async <T = unknown, U = unknown>(
241+
url: string,
242+
data: U
243+
): Promise<UnsafeResponseData<T>> => await _unsafe<T, U>('PATCH', url, data);
181244

182-
const put = async (url, data = {}) => {
183-
const resp = await _unsafe('PUT', url, data);
184-
return resp;
185-
};
245+
/**
246+
* Make a PUT call to `url`.
247+
*
248+
* The return data is the JSON response body, or `null` if there is no content. Specify
249+
* the generic type parameter `T` to get typed return data, and `U` for strongly typing
250+
* the request data (before JSON serialization).
251+
*/
252+
const put = async <T = unknown, U = unknown>(
253+
url: string,
254+
data: U
255+
): Promise<UnsafeResponseData<T>> => await _unsafe<T, U>('PUT', url, data);
186256

187-
const destroy = async url => {
257+
/**
258+
* Make a DELETE call to `url`.
259+
*
260+
* If the delete was not successfull, an error is thrown.
261+
*/
262+
const destroy = async (url: string): Promise<void> => {
188263
const opts = {
189264
method: 'DELETE',
190265
};

src/components/Anchor/Anchor.jsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Link as UtrechtLink} from '@utrecht/component-library-react';
2-
import classNames from 'classnames';
2+
import clsx from 'clsx';
33
import PropTypes from 'prop-types';
44

55
export const ANCHOR_MODIFIERS = [
@@ -18,7 +18,7 @@ const Anchor = ({
1818
...extraProps
1919
}) => {
2020
// extend with our own modifiers
21-
const className = classNames(
21+
const className = clsx(
2222
'utrecht-link--openforms', // always apply our own modifier
2323
{
2424
'utrecht-link--current': modifiers.includes('current'),

src/components/AppDebug.jsx src/components/AppDebug.tsx

+16-8
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,37 @@
11
import {FormattedDate, FormattedRelativeTime, useIntl} from 'react-intl';
22
import {useState as useGlobalState} from 'state-pool';
33

4-
import {sessionExpiresAt} from 'api';
5-
import {getVersion} from 'utils';
4+
import {sessionExpiresAt} from '@/api';
5+
import {getVersion} from '@/utils';
66

7-
const DebugInfo = ({label, value, children}) => (
7+
export interface DebugInfoProps {
8+
label: string;
9+
children: React.ReactNode;
10+
}
11+
12+
const DebugInfo: React.FC<DebugInfoProps> = ({label, children}) => (
813
<div className="debug-info">
914
<div className="debug-info__label">{label}</div>
10-
<div className="debug-info__value">{value ?? children}</div>
15+
<div className="debug-info__value">{children}</div>
1116
</div>
1217
);
1318

14-
const AppDebug = () => {
19+
const AppDebug: React.FC = () => {
1520
const {locale} = useIntl();
1621
const [{expiry}] = useGlobalState(sessionExpiresAt);
17-
const expiryDelta = (expiry - new Date()) / 1000;
1822
return (
1923
<div className="debug-info-container" title="Debug information (only available in dev)">
20-
<DebugInfo label="Current locale" value={locale} />
24+
<DebugInfo label="Current locale">{locale}</DebugInfo>
2125
<DebugInfo label="Session expires at">
2226
{expiry ? (
2327
<>
2428
<FormattedDate value={expiry} hour="numeric" minute="numeric" second="numeric" />
2529
&nbsp;(
26-
<FormattedRelativeTime value={expiryDelta} numeric="auto" updateIntervalInSeconds={1} />
30+
<FormattedRelativeTime
31+
value={(expiry.getTime() - new Date().getTime()) / 1000}
32+
numeric="auto"
33+
updateIntervalInSeconds={1}
34+
/>
2735
)
2836
</>
2937
) : (

src/components/AppDisplay.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import classNames from 'classnames';
1+
import clsx from 'clsx';
22

33
export interface AppDisplayProps {
44
/**
@@ -41,7 +41,7 @@ export const AppDisplay: React.FC<AppDisplayProps> = ({
4141
router,
4242
}) => (
4343
<div
44-
className={classNames('openforms-app', {
44+
className={clsx('openforms-app', {
4545
'openforms-app--no-progress-indicator': !progressIndicator,
4646
'openforms-app--no-language-switcher': !languageSwitcher,
4747
})}

0 commit comments

Comments
 (0)