Types test your code π¬
Type safety collapses runtime bugs to compile-time guarantees.
There are some simple best practices that will guarantee safety in your apps long-term. Here are the 3 strategies I use the most π
Brands
You are working on a blogging app, so naturally you can edit a post.
What's the signature of the editPost
function?
Here is the wrong (not ideal) answer:
const editPost = (postId: string, post: Post) => {
/// ...
}
We are not safe here. Is postId
really any string
?
The general assumption is that you get a Post
from an API, and the user can edit it. Therefore, postId
must come from the API, it's not just string
π€
Coming back to safety means using branded types (from
effect
) β¨
It works like this:
- Define a
Schema
for a branded type (Schema.brand
) - Decoded from the schema when fetching the
Post
- Use
postId
from the API to make the edit request
import { Schema } from "effect"
const PostId = Schema.String.pipe(Schema.brand("PostId"));
type PostId = typeof PostId.Type;
const Post = Schema.struct({
postId: PostId,
});
// Get `Post` from an API
const getPost: Effect<Post> = getPostFromApi.pipe(
// Decode schema for valid `Post`
Effect.flatMap(Schema.decode(Post)),
);
const editPost = (
postId: PostId,
post: Omit<typeof Post.Type, "postId">
) => {
/// ...
}
Now it's impossible to call editPost
unless you have a valid PostId
. And the only place where you get a valid PostId
is after calling getPost
.
Result: you guaranteed that editing a post is safe in all cases (all!) π―
Note: You are safe as long as
PostId
is verified. Any "exception" (e.g. usingas unknown as PostId
) when creating branded types tosses you out of safety π¬
Literal types
Safety requires strictness π
Being strict with types means allowing only the minimal set of values to cover your use case π¬
Back to the Post
example.
First, it's more safe an maintainable to avoid something like isPublished: boolean
, and instead opt for a more general status
field.
Second, don't type status: string
. Use a string literal instead:
import { Schema } from "effect"
const Post = Schema.struct({
postId: PostId,
status: Schema.Literal("published", "draft"),
});
Major advantages:
- You can extend
status
with other states (e.g.in-review
) - You avoid impossible states on a type-level
- You can pattern match on all literals
Bonus tip: you can type a
Record
key using literals. You will be required to provide values for all keys.I use this all the times for labels π§
type Language = "en" | "it" | "jp";
type LanguageLabel: Record<Language, string> = {
en: "English",
it: "Italian",
jp: "Japanese"
};
NonEmptyArray
Arrays are insidious, specifically this: arr[0]
π
Tip: instead of checking the array length to see if it's non-empty, you can check the first item when all the items are expected to be truthy. β if (arr.length > 0) β if (arr[0]) It's shorter, cleaner, and plays nicer with TypeScript's `noUncheckedIndexedAccess` setting.
First, never forget to add noUncheckedIndexedAccess
to your tsconfig.json
.
noUncheckedIndexedAccess
adds| undefined
to the typeof ofarr[0]
π
Second, verify a value exists every time you access any index in an array. With effect
you can make arr[0]
safe using NonEmptyArray
.
There is only 1 real way of checking for non-empty array π
Tip: instead of checking the array length to see if it's non-empty, you can check the first item when all the items are expected to be truthy. β if (arr.length > 0) β if (arr[0]) It's shorter, cleaner, and plays nicer with TypeScript's `noUncheckedIndexedAccess` setting.
Even without effect
, type safe arrays are a simple trick:
type NonEmptyArray = {
[0]: string;
} & string[];
// Property '[0]' is missing in type '[]' but required in type '{ 0: string; }'
const notValid: NonEmptyArray = [];
const valid: NonEmptyArray = ["valid"];
And guess what? You can even make something like ArrayWithAtLeastTwoElements
:
type ArrayWithAtLeastTwoElements = {
[0]: string;
[1]: string;
} & string[];
// Type '[]' is not assignable to type '{ 0: string; 1: string; }'
const notValid: ArrayWithAtLeastTwoElements = [];
// Property '[1]' is missing in type '[string]' but required in type '{ 0: string; 1: string; }'
const stillInvalid: ArrayWithAtLeastTwoElements = ["valid"];
const valid: ArrayWithAtLeastTwoElements = ["valid", "also-valid"];
Three weeks streak of effect
on your X feed. Everyone is jumping in:
Effect Costs: - Basically feels like learning a new language - Unfamiliar syntax - So many API's to learn - Pay the onboarding cost initially, and keep paying it as new devs come in Benefits: - A good codebase that actually stays good
If you are jumping in right now as well, join the Discord channel:
Effect community is good btw
See you next π