Start with the essentials, not the entire type system
When you first adopt TypeScript, the sheer number of features can be overwhelming. You don't need to master every advanced type trick on day one. In my experience, a handful of features deliver 80% of the value and make your code safer and more readable immediately. Here are the ones I'd adopt first.
1. Explicit function return types
TypeScript can infer return types, but being explicit about them acts as documentation and catches mistakes early. If a function's logic changes and the return type shifts, the compiler will tell you instead of silently breaking callers.
function getUser(id: string): User {
// ...
}
This is especially useful for complex functions or those used across a codebase. It also makes code review faster because the intent is clear.
2. unknown over any
any is the escape hatch that disables type checking. unknown is the safe counterpart: you can't use it without narrowing first. When dealing with data from APIs or user input, use unknown and then narrow with type guards.
function parseJson(raw: string): unknown {
return JSON.parse(raw);
}
const data = parseJson('{"name":"Ada"}');
if (typeof data === 'object' && data !== null && 'name' in data) {
console.log(data.name); // safe
}
This forces you to handle the unknown shape, preventing runtime errors.
3. Discriminated unions
When a value can be one of several shapes, a discriminated union uses a common property (the discriminant) to narrow the type. This is a clean replacement for error-prone if chains and makes exhaustive checks possible.
type Result =
| { status: 'success'; data: string }
| { status: 'error'; error: Error };
function handle(result: Result) {
switch (result.status) {
case 'success':
console.log(result.data);
break;
case 'error':
console.error(result.error.message);
break;
}
}
The compiler ensures you handle every case, and you get autocompletion for the specific properties.
4. satisfies operator (TypeScript 4.9+)
satisfies lets you check that a value matches a type without changing its inferred type. This is perfect for objects that need to meet a certain shape while keeping literal types for autocompletion.
const config = {
port: 3000,
env: 'development',
} satisfies Record<string, string | number>;
config.port; // number, not string | number
Without satisfies, you'd lose the specific literal types or have to use as which can hide errors.
5. Readonly and readonly properties
Immutability prevents accidental mutation bugs. Use readonly on properties and Readonly<T> for whole objects. It's a compile-time guarantee that doesn't affect runtime performance.
interface Point {
readonly x: number;
readonly y: number;
}
const origin: Readonly<Point> = { x: 0, y: 0 };
// origin.x = 1; // Error: Cannot assign to 'x' because it is a read-only property.
This is especially valuable in shared code or when passing objects around.
6. Utility types: Partial, Pick, Omit
These save you from writing repetitive interfaces. Partial<T> makes all properties optional, Pick<T, K> selects a subset, and Omit<T, K> excludes some. They're perfect for update payloads or derived types.
interface User {
id: number;
name: string;
email: string;
}
type UserUpdate = Partial<Omit<User, 'id'>>;
// { name?: string; email?: string }
They keep your types DRY and adapt when the base interface changes.
7. const assertions
as const makes a value deeply readonly and narrows literal types. It's great for configuration objects or constants that shouldn't change.
const COLORS = {
primary: '#3498db',
secondary: '#2ecc71',
} as const;
// COLORS.primary is '#3498db', not string
This prevents accidental reassignment and gives you literal types for pattern matching.
8. noUncheckedIndexedAccess (strictness)
Enable this compiler option. It forces you to handle the possibility that an array index or object property access might return undefined. This eliminates a whole class of runtime errors.
const arr = [1, 2, 3];
const first = arr[0]; // number | undefined with the flag on
if (first !== undefined) {
console.log(first.toFixed());
}
It's a small change that surfaces real bugs during development.
Start small, then expand
These features are a solid foundation. Once you're comfortable, you can explore advanced topics like conditional types, mapped types, and template literal types. But you don't need them to benefit from TypeScript. Adopt these first, and you'll see immediate improvements in code clarity and safety without drowning in complexity.
For more details, the TypeScript Handbook is the canonical reference, and the release notes explain new features like satisfies. Happy typing!
Top comments (0)