Guerrilla Types in TypeScript
Renegade Type Design
Guerrilla Types in TypeScript
In this article, we’ll explore three intriguing (possibly terrible?) techniques to assist in type design!
The main goal is consistent and predictable Model/Entity/Class interfaces.
- Approaches to Designing Types
- Technique #1: Why not all
- Technique #2: Mix-ins
- Technique #3: Organizing with Namespaces
- Summary
Approaches to Designing Types
You’ve probably encountered or written varying patterns around “type implementations.” Especially when consuming data from 3rd party APIs.
Note: I’m intentionally ignoring “traditional” processes building Entity Relationship Diagrams (ERD) or Object Oriented Programming (OOP) inheritance hierarchies. Here, we’re building types to represent semi-structured API data.
Let’s explore two high-level approaches: Single large object (Top-down) vs. Multiple named types (Bottom-up.)
Single large object
Prioritizes being explicit over reusability & DRY-ness.
Bonus: IDE/Dev Experience is great, since tooltips include a more complete preview - without fuss.
Since we are prioritizing explicit readability, it’s okay to indulge in some repetition (within reason.) When groups of properties repeat many times, feel free to extract the repeated fields to a named type.
Multiple named types
Prioritizing reusability & DRY-ness.
This approach is likely the favored approach by a wide margin.
Overall, this approach is great. But it’s not without drawbacks.
- Readability is excellent at first; however, it can suffer as the size & number of types grows.
- Relentlessly DRY, but at what cost? (More on this later.)
- Developer experience can suffer since tooltips are less informative.
⚠️ Since (approximately) TypeScript v3, the Language Server truncates tooltips, omitting nested properties. 💡 There are tricks to improve things a bit. Try holding
Cmd or Ctrl
, then hover over various type names - you should see at least one extra ‘layer’ of properties in the tooltip.
Why do we have to choose between these two approaches? (Big ol’ type vs. Named sub-types.)
Technique #1: Why not all
Can we have it all?
- Clarity of “big-picture” types?
- Plus named sub-types?
- Without duplication?
✅ YES! 🎉
- Create large “Primary” structured types.
- Export sub-types derived from the Primary type.
This approach really shines in systems where “high-level” objects benefit from documentation in one place. Also, this technique supports re-use between many use cases: Models, Services, Query Results, etc.
Technique #2: Mix-ins
This strategy is all about putting together the right fields, with the right names, to represent single logical objects. The goal is to efficiently address multiple use cases with TypeScript Utilities and Type Unions.
This approach differs from traditional OOP inheritance & hierarchies, which aims to create layers of objects into tightly bound taxonomies. The mix-in approach is about flat and loosely-related types, grouping related fields while reducing duplication.
Mix-in Examples
Example User
Let’s represent the User
before & after saving to the database.
Now we can sculpt the exact fields we need (like password
for create/update, but not included in queries of UserInstance
).
- “Is this a good practice?”
- “Should I try it out?”
No idea. Let’s keep going!
Technique #3: Organizing with Namespaces
Here, we declare a ModelMixins
namespace. This provides some organization plus a clearer reuse pattern.
Standardized Shapes
createdAt
&updatedAt
exist together.id
, notID
or_id
.
Using Type Unions
If desired, you can also export individual named types:
Real-world Usage
Here’s an upsert()
function that uses in
operator to distinguish between UserInstance
and UserPayload
types.
Summary
We covered three techniques and a few related supporting ideas.
You may be asking, are these good patterns? Should I adopt some of these ideas?