Development speed ποΈ
There is not enough discussion about pushing features fast, and reliably, even as the project keeps growing π²
I use a few techniques and principles to keep my speed at the top π
Type safe code = Less bugs π«‘ But how? Here is an example π Unsafe wide `string` return type ππΌββοΈ Restrict return, but only "happy path" π«‘ Schema and Option for validation and errors Types verify the function for you βοΈ
Here they are π
Types
That's key number 1.
This is the best time saver (long term). Nail this, and you are fast π«‘
When your app is fully type safe (and I mean fully), you cannot make mistakes.
Even better than that, you cannot broke older features as you add more π§±
Changing a type signature should create a cascade of compile-time errors (that you can easily fix). Full stack.
Example: OpenApi.
export class GetUserByUsername extends Request.TaggedClass("GetUserByUsername")<
typeof GetUserByUsername.schema.Type,
ClientError,
// π Add all the necessary parameters (path/query/body) from the OpenAPI schema
paths[typeof GetUserByUsername.path]["get"]["parameters"]["path"]
> {
// π The path is type-safe derived from the OpenAPI schema
static readonly path = "/2.0/users/{username}" satisfies keyof paths;
// π Response schema (can be used for validation)
static readonly schema = Schema.Struct({
username: Schema.optional(Schema.String),
uuid: Schema.optional(Schema.String),
});
}
I wire API requests to a shared OpenApi schema. As soon as any API change occurs, the app will stop compiling.
Check out "Building a type-safe bridge between client and server" for more on full-stack safety.
A few other examples:
- Change an XState event, and get compile-time errors where updates become necessary
- Add a new value to a string literal, and use pattern matching to get compile-time errors
- Use
NonEmptyArray
and branded types for strict(er) checks
TypeScript Tip: NonEmptyArray β¨ A simple trick allows to type check for arrays having at least 1 element Another step towards better type safety π
In summary, you should be able to substitute
pnpm run test
withpnpm run typecheck
(tsc
compiler checking for type errors) ππΌββοΈ
Minimal transparent configuration
Tools should make you faster.
How obvious does this sound? Not obvious enough π€·πΌββοΈ
I avoid setting up countless lint rules, multiple different compile steps, complex testing, and all. This comes from experience: adding all this configuration, to then either (a) ignore it or (b) remove it π€―
Instead, I choose tools that are mostly transparent: setting up once, works every time afterwards. Examples:
- Tailwindcss: initial configuration, and then it just works
- tsconfig: as strict as possible
- Vite/Next: (ideally) just
pnpm run dev
andpnpm run build
Avoid dependencies
Dependencies are like debt: get a feature now, pay the cost later π¬
I reduce my dependency pool to a few core and trusted group:
effect
: always present, it covers most use cases in a single unified solutionxstate
: covers all state management necessities (and I mean all)- Headless components: never recreate the wheel, I use
react-aria-components
instead
Outside of these, not much else (frontend). Anything else is 1-time and project-specific. Examples:
- Stripe (e.g.
@stripe/stripe-js
) clsx
- Segment/PostHog
openapi-fetch
Repetition, or no repetition
A key skill to learn is when to copy-paste code or when to create an abstraction to avoid repetition π€
In general, "premature" abstractions will slow you down (both in the moment and long-term).
"Premature"? Rule of thumb: copy-paste the first time, consider an abstraction after the second copy-paste.
Also, the most "isolated"/specific a feature is, the less you should consider an abstraction:
- Good abstraction: copy-text machine (generic)
- Bad abstraction: a machine for a single step in a sign up flow (too specific)
"Copy text" logic useState VS State machine β‘οΈ π useState requires manual timer and state set/reset π State machine tracks the state internally, no need of manual timer nor set/reset β All the logic is outside the component
Divide and conquer
Components should not have any business logic inside them π
Periodic reminder that a component should not have any business logic inside it Abstract it away and send events π§
Or, more in general, to each its responsibility.
You should always know where a bug may originate π
Did your user get an error when clicking to submit? Most likely a request error.
User stuck during the sign up flow? State management issue.
If all requests are in the same set of files, and all state management is inside state machines, you just fly directly to the problem π
Remember when effect
was "missing documentation"? Ain't no more excuses now, new content is coming out like mushrooms π
I had some fun with @EffectTS_ today. I made some visual effects, if you will, written in Effect + React.
Meanwhile, effect
v4 is taking shape behind the scenes, and it looks delicious π
See you next π