-
Notifications
You must be signed in to change notification settings - Fork 22
/
Copy pathstrongly-typed-http.html
247 lines (233 loc) · 9.68 KB
/
strongly-typed-http.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
<html>
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<head>
<title>
leontrolski - strongly typed http
</title>
<style>
body {margin: 5% auto; background: #fff7f7; color: #444444; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.8; max-width: 63%;}
@media screen and (max-width: 800px) {body {font-size: 14px; line-height: 1.4; max-width: 90%;}}
pre {width: 100%; border-top: 3px solid gray; border-bottom: 3px solid gray;}
a {border-bottom: 1px solid #444444; color: #444444; text-decoration: none; text-shadow: 0 1px 0 #ffffff; }
a:hover {border-bottom: 0;}
.inline {background: #b3b2b226; padding-left: 0.3em; padding-right: 0.3em; white-space: nowrap;}
blockquote {font-style: italic;color:black;background-color:#f2f2f2;padding:2em;}
details {border-bottom:solid 5px gray;}
</style>
<link href="https://unpkg.com/[email protected]/themes/prism-vs.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.20.0/components/prism-core.min.js">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.20.0/plugins/autoloader/prism-autoloader.min.js">
</script>
</head>
<body>
<a href="index.html">
<img src="pic.png" style="height:2em">
⇦
</a>
<p><i>2021-11-04</i></p>
<h1>
Strongly typed Web 1.0 with TypeScript
</h1>
<em>
Or more generically - "Using declarative descriptions to strongly type system boundaries with TypeScript".
</em>
<p>
You're building your Web 1.0 site (maybe more <a href="https://en.wikipedia.org/wiki/Web_2.0#Technologies">
Web 1.5
</a>
), you've got proper <code class="inline"><form></code>
s, <code class="inline">/urls/with/:params</code>
, <code class="inline">?request=query&parameters=here</code>
. All the good stuff. You're rendering your html in the same codebase you're serving routes from, and you're doing it all in TypeScript.
</p>
<p>
How can we strongly type all the interactions between client and server so that we can't make stupid mistakes like:
</p>
<pre class="language-html"><code><form method="POST" action="/foo">
<input name="achieve">
</form></code>
</pre>
<p>
With a typo in our Express app:
</p>
<pre class="language-javascript"><code>app.post("/foo", (req, res) => {
const achieve = req.body.acheive // note the typo!
...
})</code>
</pre>
<h2>
Let's go!
</h2>
<p>
I'm going to come back to the implementation, for now let's just write some staticly typed GET links. Firstly, let's describe our routes:
</p>
<pre class="language-javascript"><code>import * as http from "./lib/http" // our small helper library - see below
const get = {
index: {
url: "/",
params: [],
query: [],
queryNotRequired: [],
},
"/v1/:foo": {
url: "/v1/:foo",
params: ["foo"],
query: ["a", "b"],
queryNotRequired: ["c"],
},
} as const
const GET = http.makeGET(get)</code>
</pre>
<ul>
<li>
First we have our index, this has no params and no query parameters - simple.
</li>
<li>
Next we have the route <code class="inline">/v1/:foo</code>
, this has one param and three query parameters, two of which are required.
</li>
</ul>
<p>
In our template, we now want to make a link like:
</p>
<pre class="language-html"><code><a href="/v1/hey?a=A&b=B">...</a></code>
</pre>
<p>
We're dynamically rendering the element from <a href="https://reactjs.org/docs/react-dom-server.html">
React
</a>
or <a href="https://github.com/MithrilJS/mithril-node-render#mithril-node-render">
Mithril
</a>
or whatever.
</p>
<p>
With our declarative definition set up, we can now use:
</p>
<pre class="language-javascript"><code>href = GET["/v1/:foo"].makeUrl({foo: "hey"}, {a: "A", b: "B"})</code>
</pre>
<p>
Let's see with typechecking in VSCode:
</p>
<video autoplay loop muted src="videos/get-from-client.webm" style="
margin: auto;
display: block;
max-width: calc(min(80em, 100%));
box-shadow: 0px 0px 6px 2px #0000002b;
margin-bottom: 2em;
">
</video>
<p>
All our parameters were typechecked, wahoo!
</p>
<p>
Now, let's look at the server-side in Express:
</p>
<pre class="language-javascript"><code>app.get(GET["/v1/:foo"].url, (req, res) => {
const { params, query } = GET["/v1/:foo"].req(req)
})</code>
</pre>
<p>
And again with typechecking:
</p>
<video autoplay loop muted src="videos/get-from-server.webm" style="
margin: auto;
display: block;
max-width: calc(min(30em, 100%));
box-shadow: 0px 0px 6px 2px #0000002b;
margin-bottom: 2em;
">
</video>
<p>
A very similar approach is taken for POST routes.
</p>
<p>
The description:
</p>
<pre class="language-javascript"><code>const post = {
"/v2/:bar": {
url: "/v2/:bar",
params: ["bar"],
body: ["email", "message"],
},
} as const
const POST = http.makePOST(post)</code>
</pre>
<p>
During rendering:
</p>
<pre class="language-javascript"><code>const form = POST["/v2/:bar"].makeForm({bar: "there"})</code>
</pre>
<p>
Where <code class="inline">form</code>
has the following to help build your html: <code class="inline">.form</code>
, <code class="inline">.inputs</code>
, <code class="inline">.assertAllInputsReferenced()</code>
- this final bit uses some <code class="inline">Proxy</code>
magic to ensure you reference all the form body parts that you needed to.
</p>
<p>
Now for the server-side usage in Express:
</p>
<video autoplay loop muted src="videos/post-from-server.webm" style="
margin: auto;
display: block;
max-width: calc(min(30em, 100%));
box-shadow: 0px 0px 6px 2px #0000002b;
margin-bottom: 2em;
">
</video>
<br>
<br>
<h2>
How does it work?
</h2>
<p>
I'm not going to go into crazy detail, you can inspect <a href="https://github.com/leontrolski/strongly-typed-http/blob/main/http.ts">
the source
</a>
yourself. In summary, these bits of TypeScript were used heavily (see the video below):
</p>
<pre class="language-javascript"><code>const l = ["a", "b"] as const // make a value's interior visible to TypeScript
const foo = {bar: l} as const
type Bar = (typeof foo)["bar"] // convert readonly -> type and access a property
type BarUnion = Bar[number] // convert literal[] -> union of literals
type BarMap = { [K in BarUnion]?: string} // make a map from a union of strings
const maker = <L extends readonly string[]>(l: L): L[number] => l[0] // use generics
const made = maker<typeof l>(l)</code>
</pre>
<video autoplay loop muted src="videos/nice-typescript.webm" style="
margin: auto;
display: block;
max-width: calc(min(80em, 100%));
box-shadow: 0px 0px 6px 2px #0000002b;
margin-bottom: 2em;
">
</video>
<h2>
Conclusions
</h2>
<ul>
<li>
I now have compile time type safety for my old-skool web pages, feels good.
</li>
<li>
The big takeaway for me was - use <code class="inline">as const</code>
+ generics to bridge the run-time/compile-time boundary.
</li>
<li>
There are probably more generic ways of doing this kind of thing, <a href="https://gcanti.github.io/io-ts/modules/Kleisli.ts.html#fromstruct">
io-ts
</a>
is in this space (but requires you and your colleagues to graduate from Kleisli's school of Functors and Monadry).
</li>
<li>
In Python land <a href="https://fastapi.tiangolo.com/#example-upgrade">
FastAPI
</a>
has done a brilliant job of doing this kind of thing, with nice validation methods and swagger docs baked in. If you haven't seen it, look through the docs - every language should have an equivalent framework.
</li>
</ul>
</body>
</html>