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 ๐
