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
Schemafor a branded type (Schema.brand) - Decoded from the schema when fetching the
Post - Use
postIdfrom 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
PostIdis 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
statuswith 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
Recordkey 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.
noUncheckedIndexedAccessadds| undefinedto 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 π
