Programming is art ๐จ
Just like other arts, every software has the imprint of its author. The main difference is its expression: code lives in a more stringent set of deterministic rules.
The more stringent the rules, the more the software becomes succinct, safe, and bug free ๐
Strict software is type-safe, here is how ๐
Flexible or strict?
The tension in software development is between code that is flexible and easy to change, and code that is strict and precise:
- Dynamic languages opt for full flexibility: anything can be anything, and anything can change everywhere in any way
- Pure functional languages opt for complete strictness: everything must be explicit and fitting
You can go full strictness with TypeScript:
const NonNegative = Schema.Number.pipe(Schema.nonNegative());
const Positive = Schema.Number.pipe(Schema.positive());
type NonNegative = typeof NonNegative.Type;
type Positive = typeof Positive.Type;
// ๐ Everything is checked by strict typing rules
const fun = (params: { a: NonNegative; b: Positive }): Positive => {
/// ...
}
The cost is verbosity, flexibility, and explicit complexity:
fun
can only be used with those exact types, nothing else- All the types must be manually specified
- The code looks long and complex
The language is JavaScript, so you can go on the other dynamic extreme with the same result:
const fun = (params) => {
/// ...
}
Same exact code at the end. From a user perspective, the result is the same.
So, why am I urging you to choose the first more verbose solution over the first?
Type safety is a long term investment
The difference between good and bad "software engineering" is all in the "engineering" part, and less about the "software" one.
Inexperienced devs (and the current generation of AI ๐๐ผโโ๏ธ) focus on coding, while more experienced devs spend more time on "engineering"
Engineering is about the whole construction over the long-term. You want something that works, keeps working, and resilient to changes.
Type safety acts as a solid foundation that prevents software from breaking ๐๏ธ
As such, it requires an initial longer implementation time, but it reaps benefits in the future:
- Changes are faster and safer (most changes take a few minutes)
- I don't implement tests, but I hardly find bugs, even after a new update
Example: typeonce.dev contains all the content as a mix of mdx
and json
.
Extracting all the content requires reading and validating all the files. Nonetheless, the website is (mostly) bug free, and every update is a matter of minutes:
That's because the code is extremely strict: branded types, schemas, explicit errors.
Cons of type safety
What's the catch? Here you are:
- Explicit complexity
- Longer initial implementation times
- Less flexible to changes
The code gets longer, (apparently) more complex, and major changes require whole restructuring.
The source code of Typeonce is not easy to understand or modify for a beginner.
export class Course extends Effect.Service<Course>()("Course", {
accessors: true,
dependencies: [Metadata.Default, Content.Default],
effect: Effect.gen(function* () {
const { getCourse } = yield* Content;
const metadataService = yield* Metadata;
return {
full: ({ slug }: { slug: string }) =>
Effect.gen(function* () {
const { metadata, content, dependencies } = yield* getCourse({
slug,
});
return yield* Effect.all(
{
dependencies: Effect.succeed(dependencies),
metadata: metadataService.course({ source: metadata }),
modules: Effect.all(
Array.map(content, (module) =>
Effect.gen(function* () {
const metadata = yield* metadataService.module({
source: module.metadata,
});
const lessonsMetadata = yield* Effect.all(
Array.map(module.lessonsMetadata, (lesson) =>
Effect.gen(function* () {
const metadata = yield* metadataService.lesson({
source: lesson.metadata,
});
const lessonSlug = yield* Schema.decodeEither(
SlugWithoutIndex
)(lesson.slug);
return {
metadata,
slug: lessonSlug,
};
})
)
);
const moduleSlug = yield* Schema.decodeEither(
SlugWithoutIndex
)(module.slug);
return { slug: moduleSlug, metadata, lessonsMetadata };
})
)
),
},
{ concurrency: 2 }
);
}),
};
}),
}) {}
The example above is a single service to read metadata for a course. Everything is type-safe back to front.
This ensures no bugs even after changes, but the cost is paid in code complexity.
You can find many examples of what I mean with type-safe code on Typeonce. Some recent updates:
Combine React 19 + @EffectTS_ with useActionState Custom hook to run an effect as action while also reporting errors in the state All composable and type safe โจ
The tickets for localfirstconf are out now! See you there if you decide to come ๐ซก
See you next ๐