file-typeEngineering
Typing props and hooks, generics, narrowing, discriminated unions, and the utility types worth knowing.
1 item
Type the boundaries, let inference do the rest
Annotate function signatures, component props, and API payloads. Inside a function, let TypeScript infer — over-annotating local variables is noise. The goal is types where data enters your code (props, fetch responses, form input), so everything downstream is checked for free.
Typing components
type ButtonProps = {
variant: "primary" | "ghost";
disabled?: boolean;
onClick: () => void;
children: React.ReactNode;
};
function Button({ variant, disabled = false, onClick, children }: ButtonProps) { ... }
React.ReactNode for children, not string."primary" | "ghost") over string — they document valid values and autocomplete.type Props = { variant: ... } & React.ComponentProps<"button">.Discriminated unions — the most useful pattern
Model "states that can't coexist" so impossible states are unrepresentable:
type Result =
| { status: "loading" }
| { status: "error"; message: string }
| { status: "success"; data: User };
Switching on status narrows the type — TypeScript knows data only exists in the success branch. This kills a whole class of "cannot read property of undefined" bugs.
Narrowing & guards
typeof, in, and custom type guards (function isUser(x): x is User) narrow unknown/union types safely.unknown and validate it (Zod) rather than casting with as. as is a lie to the compiler — use it sparingly.Utility types worth memorizing
Partial<T>, Required<T>, Pick<T, K>, Omit<T, K> for reshaping.Record<K, V> for maps/dictionaries.ReturnType<typeof fn> and Awaited<T> to derive types from values instead of duplicating them.z.infer<typeof schema>) so types and runtime validation never drift.Config
strict mode. The whole value of TS evaporates without it. Add noUncheckedIndexedAccess for an extra safety net on array/object access.