10 TypeScript Patterns That Catch Bugs at Compile Time, Not Runtime
theme: smartblue
Welcome to follow the WeChat public account: FSA Full-Stack Action 👋
When writing for the Web, it's a waste to treat TypeScript merely as a "bug checker." In real engineering practice, TypeScript helps us write safer, cleaner code and can even block many low-level logic errors at compile time.
While working on Web projects, I've summarized these 10 highly practical techniques, hoping they can improve your development experience.
1. Advanced Basics: Reducing Redundancy and Improving Accuracy
When dealing with basic type definitions, many developers tend to write repetitive and imprecise code.
1. Use satisfies instead of type assertions
Previously, when specifying types for objects, I habitually used as. But as is essentially a way of "forcing the compiler to listen to me," which can mask potential configuration errors.
Now I prefer using satisfies. It can both validate the type and preserve the object's most primitive literal information.
| Method | Validation Capability | Type Precision | Risk |
|---|---|---|---|
Using as |
Forced conversion | Loses specific literal info, downgrades to broad type | Easily masks incorrect config values |
Using satisfies |
Strict validation | Preserves the most precise literal type | Safe and accurate |
// Not recommended: using as
const config = {
theme: "dark",
language: "en",
} as AppConfig;
// If AppConfig requires theme to be "light", this might not error, but runtime logic will break
// Recommended: using satisfies
const config = {
theme: "dark",
language: "en",
} satisfies AppConfig;
// If the config doesn't match AppConfig, it will error directly here
2. Use typeof to reuse types
Sometimes we define a complex object but later want to generate an interface based on it. Instead of manually writing an almost identical structure, just use typeof.
const user = {
id: 1,
name: "John",
};
// Directly reuse the object's structure, no need to manually write an Interface again
type User = typeof user;
3. Use keyof to extract key names
When maintaining a large interface, if you need to define a set of fields, never manually write type Fields = "id" | "name" | .... With this approach, if the interface adds a property later, you have to manually update this type, which is very easy to miss.
interface User {
id: number;
name: string;
email: string;
}
// Automatically get all property names; when new properties are added, this syncs automatically
type UserFields = keyof User;
4. Use as const to fix literals
When defining constant configurations, TypeScript by default infers values as the broad string type. But when dealing with Redux actions or state machines, we need it to maintain the precise literal value.
// Without as const, the type of STATUS.SUCCESS is string
const STATUS = {
SUCCESS: "success",
};
// With as const, the type of STATUS.SUCCESS is "success"
const STATUS = {
SUCCESS: "success",
} as const;
5. Let inference do the work; don't manually annotate types
This is a small habit, but it makes the code look much cleaner. TypeScript's inference ability is actually very strong; there's no need to write const count: number = 10; everywhere.
// No need to write this
const count: number = 10;
// Just write it directly; the compiler knows what's up
const count = 10;
2. Flexible Use of Type Tools: Handling Business Scenarios
When dealing with API data or component Props, Record, Partial, Pick, and Omit are absolute productivity tools.
1. Use Record to build mappings
When you need to define a key-value object where the key range is fixed (like color configurations for various statuses), Record is very useful. It forces you to implement all keys, preventing you from missing a status.
type Status = "success" | "warning" | "danger";
// Forces you to write the corresponding colors for all three statuses
const colors: Record<Status, string> = {
success: "#22c55e",
warning: "#f59e0b",
danger: "#ef4444",
};
2. Use Partial for partial updates
When building an edit page or making a PATCH request, we usually only need to submit some fields. Using Partial can quickly generate a type where "all properties become optional."
interface User {
id: number;
name: string;
email: string;
}
// type UpdateUser = { id?: number; name?: string; email?: string; }
type UpdateUser = Partial<User>;
const updateUser = (data: UpdateUser) => {
// ...
};
3. Use Pick and Omit to streamline interfaces
Sometimes an interface is too heavy (containing sensitive info or unnecessary fields). We can use Pick (select) or Omit (exclude) to quickly get a lightweight version of the type.
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Only email and password, for login
type LoginUser = Pick<User, "email" | "password">;
// Exclude password, for public display
type PublicUser = Omit<User, "password">;
3. Advanced Practice: Generic Components and Rigorous Logic
1. Build highly reusable components with Generics
If you write a Select dropdown component and want it to render both a user list and a product list, then Generics is the only choice.
interface SelectProps<T> {
items: T[];
renderItem(item: T): React.ReactNode;
}
// Written this way, no matter what type of items are passed in, the component can accurately capture the type
function MySelect<T>({ items, renderItem }: SelectProps<T>) {
return (
<ul>
{items.map((item, idx) => (
<li key={idx}>{renderItem(item)}</li>
))}
</ul>
);
}
2. Use union types to prevent incorrect Prop combinations
This is a very interesting scenario. Suppose you write a Button component that is either a link (with an href attribute) or a regular button (with an onClick attribute), but logically it shouldn't have both.
If you use traditional optional properties href?: string; onClick?: () => void;, a developer might pass both properties simultaneously, causing logical chaos. We can use union types to lock down this "mutually exclusive" relationship.
type ButtonProps =
| { href: string; onClick?: never }
| { onClick: () => void; href?: never };
// Defined this way:
// <Button href="/home" /> -> OK
// <Button onClick={handleClick} /> -> OK
// <Button href="/home" onClick={handleClick} /> -> Error! (TS will report an error)
Finally
The core value of TypeScript is not just about catching bugs; it provides a way for us to describe business logic and component boundaries in a "declarative" manner.
You don't need to learn all 10 techniques at once. I suggest that during development, when you feel you are writing repetitive type definitions or that a component's Props boundaries are blurry, come back and look at this set of solutions.
If you find this content helpful, feel free to discuss it in the comments.
If the article has been helpful to you, please don't hesitate to click and follow my WeChat public account: FSA Full-Stack Action, which will be the greatest encouragement to me. The public account covers not only Android technology but also iOS, Python, and other articles; there might be skill points you want to learn about~
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
You said it's a literal, so it should be const colors = { success: "#22c55e", warning: "#f59e0b", danger: "#ef4444", } as const