The TypeScript Type Features You Actually Use Daily: A Quick TypeScript Cheat Sheet
A practical TypeScript cheat sheet covering interfaces vs types, unions and intersections, generics, utility types like Partial and Pick, and type narrowing.
The TypeScript Type Features You Actually Use Daily
Most TypeScript tutorials open with let x: number = 1 and then never get to the things you reach for ten times an hour. The features that earn their keep are narrower and more specific: an interface that another file extends, a discriminated union the compiler refuses to let you under-handle, a Partial<User> that saves you from hand-writing a parallel type. This is a working reference for those features — the ones I find myself typing on autopilot — with real snippets you can paste and adapt.
Interface vs Type: the decision you make first
Every new shape forces this choice, and the rule is simpler than the debates suggest. Use an interface for an object shape that other code extends or implements. Use a type alias for anything that isn't a plain object: unions, tuples, mapped types, conditional types.
interface User {
id: number;
name: string;
}
interface Admin extends User {
permissions: string[];
}
// type can do object shapes too, but it shines on the rest:
type Id = number | string;
type Point = [x: number, y: number];
type Maybe<T> = T | null;
Interfaces auto-merge across declarations (useful for augmenting third-party types), and class implementations and error messages stay readable. The moment you need a union or a computed shape, an interface can't express it — switch to type. For a flat object the two are interchangeable, so default to interface and reach for type when you hit its limits.
Unions, intersections, and narrowing
A union (A | B) means "one of these." An intersection (A & B) means "all of these at once." Unions are where TypeScript's checking earns its reputation, because the compiler makes you prove which member you're holding before you use it.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
default:
// exhaustiveness check: if a new kind is added,
// this line stops compiling.
const _never: never = shape;
return _never;
}
}
That const _never: never = shape is the trick worth memorizing. Add a "triangle" variant to Shape and forget to handle it, and the assignment fails to compile because shape is no longer never in the default branch. One line turns a forgotten case into a build error instead of a 2am runtime bug.
Narrowing has several flavors beyond the discriminated kind switch: typeof x === "string", x instanceof Date, "id" in obj, and user-defined guards:
function isUser(value: unknown): value is User {
return typeof value === "object" && value !== null && "id" in value;
}
Generics: writing one function for many types
A generic is a type parameter — a placeholder filled in at the call site. The bare version is just <T>, but the constrained version (T extends ...) is what you'll write most, because it lets the function read a property while staying generic.
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
// constrained: only accept objects that have the key K
function prop<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const u: User = { id: 1, name: "Ada" };
const n = prop(u, "name"); // n is string, not any
prop(u, "email"); // compile error: "email" is not a key of User
K extends keyof T is the pattern behind half the typed utility functions you'll ever write. The return type T[K] (an indexed access type) means the result stays exactly as narrow as the property you asked for.
Utility types: derive, don't duplicate
TypeScript ships a standard library of type transformers so you never hand-write a parallel shape. The four I touch daily:
Partial<T>— every property optional. Perfect for update payloads.Required<T>— every property required.Pick<T, Keys>— keep only the listed keys.Omit<T, Keys>— drop the listed keys.
type User = { id: number; name: string; email: string };
type UserPatch = Partial<User>; // { id?, name?, email? }
type PublicUser = Omit<User, "email">; // { id, name }
type Credentials = Pick<User, "email">; // { email }
Also worth knowing: Record<K, V> for dictionaries, ReturnType<typeof fn> to grab a function's result type, and Awaited<T> to unwrap a promise. These compose — Awaited<ReturnType<typeof fetchUser>> reads "the resolved value of whatever fetchUser returns" without importing a single extra type.
A worked scenario: deriving a form type from your model
Here's the kind of thing that comes up constantly. You have a User model, and now you need a type for the "edit profile" form, which exposes only name and email, both optional because the user might change just one. Instead of writing a brand new type by hand (and forgetting to update it when User changes), derive it:
type User = {
id: number;
name: string;
email: string;
createdAt: Date;
};
// Step 1: keep only the editable fields.
type EditableFields = Pick<User, "name" | "email">;
// Step 2: make them all optional for a patch form.
type ProfileForm = Partial<EditableFields>;
// => { name?: string; email?: string }
Now rename email to emailAddress in User, and Pick<User, "name" | "email"> immediately errors — the compiler points you straight at the form type that needs updating. That feedback loop is the entire reason to compose utility types instead of copying fields. One source of truth, and the derived types fail loudly when the source moves.
When I'm reviewing pull requests, this is the single change I suggest most: someone has hand-written a 20-line "patch" interface that mirrors a model, and I leave one comment swapping it for Partial<Pick<Model, ...>>. The diff shrinks, the duplication disappears, and the two types can never drift apart again. I keep the TypeScript cheat sheet open in a side tab during reviews precisely so I can copy the exact Pick/Omit/satisfies snippet into a comment without breaking flow.
satisfies vs as: validate without widening
One last feature that changed how I write config objects. The satisfies operator (TypeScript 4.9+) checks that a value matches a type without widening the inferred type — so you get compile-time validation and keep each literal narrow for downstream lookups.
type Theme = Record<string, string>;
const theme = {
primary: "#0066ff",
danger: "#ff3344",
} satisfies Theme;
theme.primary.toUpperCase(); // ok: still typed as the literal string
// theme.primry — typo fails the build
Compare that to as Theme, which is an unchecked cast — it tells the compiler to trust you and hides any mismatch until runtime. Reach for satisfies first, as const when you want to freeze literals, and as Foo only when you genuinely know something the compiler can't.
Keep the reference handy
These features — interfaces vs types, unions with narrowing, constrained generics, utility types, and satisfies — cover most of the TypeScript you'll write in a normal week. The full searchable TypeScript cheat sheet has 100+ snippets including mapped types, infer, template literal types, and a pitfalls section, all filtering in your browser with zero network requests. If your stack spans more than types, the Python cheat sheet follows the same format for the backend side.
Made by Toolora · Updated 2026-06-13