today i learned
Tiny posts about stuff I like.
03 Dec, 2024: Type predicates to avoid casting
Type predicates have been around but today I found a particularly nice application. The situation is this: I have an interface that has an optional field, where the presence of that field means I need to create a new object on the server, and the lack of the field means the object has already been created and I'm just holding on to it for later. Here's what it looked like:
interface Thing {
name: string
blob?: File
}
const things: Thing[] = [
/* ... */
]
const uploadNewThings = (things: (Thing & { blob: File })[]) =>
Promise.all(things.map((thing) => createThing(thing.name, thing.blob)))
The intersection type Thing & { blob: File }
means that uploadNewThings
only
accepts things
that have the field blob
. In other words, things that need to
be created on the server because they have blob content.
However, TypeScript struggles if you try to simply filter the list of things
before passing it into uploadNewThings
:
uploadNewThings(things.filter((thing) => !!thing.blob))
The resulting error is this long stream of text:
Argument of type 'Thing[]' is not assignable to parameter of type '(Thing & { blob: File; })[]'.
Type 'Thing' is not assignable to type 'Thing & { blob: File; }'.
Type 'Thing' is not assignable to type '{ blob: File; }'.
Types of property 'blob' are incompatible.
Type 'File | undefined' is not assignable to type 'File'.
Type 'undefined' is not assignable to type 'File'.
The tl;dr being that despite filtering things
by thing => !!thing.blob
,
TypeScript does not recognize that the return value is actually
Thing & { blob: File }
.
Now you could just cast it,
things.filter((thing) => !!thing.blob) as (Thing & { blob: File })[]
But casting is bad! It's error-prone and doesn't really solve the problem that TypeScript is hinting at. Instead, use a type predicate:
const hasBlob = (t: Thing): t is Thing & { blob: File } => !!t.blob
uploadNewThings(things.filter(hasBlob))
With the type predicate (t is Thing & ...
) I can inform TypeScript that I do
in fact know what I'm doing, and that the call to filter
results in a
different interface.
15 Nov, 2024: Running and writing
Most runners run not because they want to live longer, but because they want to live life to the fullest. If you're going to while away the years, it's far better to live them with clear goals and fully alive than in a fog, and I believe running helps you do that. Exerting yourself to the fullest within your individual limits: that's the essence of running, and a metaphor for life—and for me, writing as well. - Haruki Murakami
13 Nov, 2024: Data migrations with data-migrate
What I traditionally would've used Rake tasks for has been replaced with data-migrate, a little gem that handles data migrations in the same way as Rails schema migrations. It's the perfect way to automate data changes in production, offering a single pattern for handling data backfills, seed scripts, and the like.
The pros are numerous:
- Data migrations are easily generated via CLI and are templated with an
up
anddown
case so folks think about rollbacks. - Just like with Rails schema migrations, there's a migration ID kept around that ensures data migrations are run in order. Old PRs will have merge conflicts.
- You can conditionally run data migrations alongside schema migrations with
bin/rails db:migrate:with_data
.
It's a really neat gem. I'll probably still rely on the good ol' Rake task for
my personal projects, but will doubtless keep data-migrate
in the toolbox for
teams.
09 Nov, 2024: Cool Rails concerns
There's something super elegant about Writebook's
use of concerns. I especially like Book:Sluggable
:
module Book::Sluggable
extend ActiveSupport::Concern
included do
before_save :generate_slug, if: -> { slug.blank? }
end
def generate_slug
self.slug = title.parameterize
end
end
Here's a few reasons:
- Nesting concerns in a model folder is neat when that concern is an
encapsulation of model-specific functionality:
app/models/book/sluggable.rb
. - Concerns don't have to be big. They do have to be single-purpose.
- Reminds me of a great article by Jorge Manrubla: Vanilla Rails is plenty. Down with service objects!
26 Oct, 2024: Kafka on the Shore
On the inside cover of Kafka on the Shore Murakami explains how his idea for the book started with its title. This approach is opposite to anything I've ever written, though I recognize there's a notable difference between fiction and technical writing. But what a powerful idea: a simple phrase shapes the entire story.
I dug up this quote from an interview:
When I start to write, I don’t have any plan at all. I just wait for the story to come. I don’t choose what kind of story it is or what’s going to happen. I just wait.
I think that's pretty cool.