Can you write this from memory?
Declare a variable `name` with type `string`
TypeScript is meant to speed you up: fewer runtime bugs, better autocomplete, safer refactors. But most pain comes from not recognizing the core syntax patterns--especially narrowing, generics, and "why is this value possibly undefined?"
This page is a practice hub: small, repeatable drills focused on the 20% of TypeScript syntax you'll use 80% of the time.
Types disappear at runtime. TypeScript catches bugs at compile time, improves autocomplete, and makes refactors safer—without changing the JavaScript your code produces.
TypeScript doesn't change your runtime code. It adds a compile-time check that catches bugs before they happen:
// This TypeScript...
function greet(name: string): string {
return `Hello, ${name}`;
}
// ...compiles to this JavaScript
function greet(name) {
return `Hello, ${name}`;
}
The types disappear at runtime. They exist to help you, your IDE, and your teammates understand and validate the code. For a detailed look at what changes when moving from plain JavaScript, see the JavaScript vs TypeScript variables comparison. Research backs this up: a 2017 study published at ICSE (Gao et al., "To Type or Not to Type") found that about 15% of publicly reported JavaScript bugs could have been detected by TypeScript's type system. Airbnb's engineering team separately reported that 38% of production bugs they analyzed were preventable with static types.
Use
interfacefor extendable objects and declaration merging. Usetypefor unions, intersections, and primitives. For plain objects, either works—pick one convention and be consistent.
Both can define object shapes. Here's when to pick each:
| Use case | Recommendation |
|---|---|
| Object shape | Either (be consistent) |
| Extending/inheriting | interface (cleaner syntax) |
| Declaration merging | interface (only option) |
| Unions | type (only option) |
| Primitives/tuples | type (only option) |
| Mapped types | type (only option) |
// Interface: extendable
interface User {
id: number;
name: string;
}
interface Admin extends User {
permissions: string[];
}
// Type: flexible composition
type Status = 'active' | 'inactive' | 'pending';
type Response<T> = { ok: true; data: T } | { ok: false; error: string };
The practical rule: Use interface for objects you might extend. Use type for everything else. Or just pick one and be consistent--the difference rarely matters. The JavaScript vs TypeScript interfaces comparison explores this topic in depth.
Use
typeof,in,instanceof, equality checks, or type guard functions (x is T) to narrow broad types. TypeScript tracks what you've checked and refines the type within each code block.
Within a code block, TypeScript tracks what you've already checked and refines the type:
function process(value: string | number | null) {
// Here: value is string | number | null
if (value === null) {
return; // Early return
}
// Here: value is string | number (null eliminated)
if (typeof value === 'string') {
console.log(value.toUpperCase()); // value is string
} else {
console.log(value.toFixed(2)); // value is number
}
}
Narrowing techniques
| Technique | Narrows |
|---|---|
typeof x === 'string' | Primitives |
x instanceof Date | Class instances |
'key' in obj | Objects with property |
x === null | Null checks |
Array.isArray(x) | Arrays |
isUser(x) | Type guard function |
Type guard functions
For complex validation, write a type guard:
interface User {
id: number;
name: string;
}
function isUser(x: unknown): x is User {
if (typeof x !== 'object' || x === null) return false;
if (!('id' in x) || typeof x.id !== 'number') return false;
if (!('name' in x) || typeof x.name !== 'string') return false;
return true;
}
// Usage
function handleData(data: unknown) {
if (isUser(data)) {
console.log(data.name); // TypeScript knows it's User
}
}
For production apps, consider libraries like Zod or ArkType that generate type guards from schemas. They handle edge cases and provide better error messages. For a side-by-side look at how TypeScript's type system compares to plain JavaScript, see the JavaScript vs TypeScript types comparison.
Generics let you write code that works with any type while preserving type information:
// Without generics: loses type info
function firstAny(arr: any[]): any {
return arr[0];
}
// With generics: preserves type info
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = first([1, 2, 3]); // type: number | undefined
const str = first(['a', 'b']); // type: string | undefined
Generic constraints
Use extends to constrain what types are allowed:
// T must have a length property
function logLength<T extends { length: number }>(item: T): void {
console.log(item.length);
}
logLength('hello'); // OK: string has length
logLength([1, 2, 3]); // OK: array has length
logLength(42); // Error: number doesn't have length
The keyof + extends pattern
This is the most common generic pattern--safely accessing object properties. The JavaScript vs TypeScript functions comparison shows how generics improve function signatures:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: 'Alice' };
getProperty(user, 'name'); // OK, returns string
getProperty(user, 'foo'); // Error: 'foo' not in keyof User
Built-in utility types save you from writing repetitive type definitions:
Essential utility types
| Utility | Does | Example |
|---|---|---|
Partial<T> | All properties optional | Form state, updates |
Required<T> | All properties required | Validated config |
Pick<T, K> | Keep only certain keys | API responses |
Omit<T, K> | Remove certain keys | Hide internal fields |
Record<K, V> | Object with key type K, value type V | Lookup tables |
Readonly<T> | All properties readonly | Immutable data |
interface User {
id: number;
name: string;
email: string;
password: string;
}
// For updates: all optional except you can't change id
type UserUpdate = Partial<Omit<User, 'id'>>;
// For public display: no password
type PublicUser = Omit<User, 'password'>;
// For creating: required fields only
type CreateUser = Pick<User, 'name' | 'email' | 'password'>;
A discriminated union uses a common property (the "discriminant") to tell types apart:
type ApiResponse<T> =
| { status: 'success'; data: T }
| { status: 'error'; error: string }
| { status: 'loading' };
function handleResponse(res: ApiResponse<User>) {
switch (res.status) {
case 'success':
console.log(res.data.name); // data exists here
break;
case 'error':
console.log(res.error); // error exists here
break;
case 'loading':
console.log('Loading...');
break;
}
}
Exhaustive checking with never
Ensure you handle all cases:
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${x}`);
}
function handleResponse(res: ApiResponse<User>) {
switch (res.status) {
case 'success': return res.data;
case 'error': throw new Error(res.error);
case 'loading': return null;
default:
return assertNever(res); // Error if a case is missed
}
}
satisfies (TypeScript 4.9+) validates a value matches a type while keeping precise inference:
// Without satisfies: type widened to Record<string, string>
const routes1: Record<string, string> = {
home: '/',
about: '/about',
};
routes1.home; // type: string
// With satisfies: type is precise
const routes2 = {
home: '/',
about: '/about',
} satisfies Record<string, string>;
routes2.home; // type: '/' (literal)
// And you still get validation
const routes3 = {
home: '/',
about: 123, // Error: number not assignable to string
} satisfies Record<string, string>;
Use satisfies when: You want to validate a shape but preserve the exact inferred types (especially literal types).
TS2322: Type 'X' is not assignable to type 'Y'
You're assigning a value that doesn't match the expected type.
// Error
let name: string = 42;
// Fixes: correct the type or the value
let name: string = 'Alice';
let count: number = 42;
TS2339: Property 'X' does not exist on type 'Y'
You're accessing a property that TypeScript doesn't know exists.
// Error
const obj: object = { name: 'Alice' };
console.log(obj.name); // Property 'name' does not exist on 'object'
// Fix: use a more specific type
const obj: { name: string } = { name: 'Alice' };
console.log(obj.name); // OK
TS18046: 'X' is of type 'unknown'
You're using an unknown value without narrowing it first.
// Error
function process(data: unknown) {
console.log(data.name); // Error: unknown
}
// Fix: narrow first
function process(data: unknown) {
if (typeof data === 'object' && data !== null && 'name' in data) {
console.log((data as { name: string }).name); // OK
}
}
TS2345: Argument of type 'X' is not assignable to parameter of type 'Y'
Function argument doesn't match parameter type.
// Error
function greet(name: string) { ... }
greet(42); // Error: number not assignable to string
// Fix: pass correct type
greet('Alice');
TS7006: Parameter 'x' implicitly has an 'any' type
You have strict or noImplicitAny enabled and a parameter has no type annotation.
// Error (with noImplicitAny)
function double(n) { // Parameter 'n' implicitly has 'any' type
return n * 2;
}
// Fix: add type annotation
function double(n: number) {
return n * 2;
}
This is the most common error when enabling strict mode on an existing project. Fix by adding explicit types to function parameters.
| Pattern | Code |
|---|---|
| Optional property | name?: string |
| Union type | string | number |
| Intersection type | A & B |
| Type assertion | value as Type (use sparingly) |
| Non-null assertion | value! (use sparingly) |
| Type guard | function isX(x): x is X { ... } |
| Generic function | function f<T>(x: T): T { ... } |
| Generic constraint | <T extends SomeType> |
| Indexed access | T[K] |
| keyof | keyof T (union of keys) |
| satisfies | value satisfies Type |
| as const | { x: 1 } as const (literal types) |
- TypeScript Handbook
- TypeScript: Narrowing
- TypeScript: Generics
- TypeScript: Utility Types
- TypeScript: satisfies operator
- TypeScript Error Codes Reference
For dedicated cheatsheets on generics, utility types, type narrowing, and the satisfies operator, see the TypeScript reference page.
When to Use TypeScript
- You're shipping JS but want safer refactors and fewer runtime surprises.
- You need reliable contracts for API data, component props, and shared libraries.
- You want to understand compiler errors fast (and stop fighting the type checker).
- You're building libraries that need to work with any type (generics).
Check Your Understanding: TypeScript
Define a User type and write a type guard that narrows unknown input to User.
Use `unknown` + a predicate function `isUser(x): x is User` that checks required keys and types, then rely on that guard for safe access.
What You'll Practice: TypeScript
Common TypeScript Pitfalls
- `any` silently removes most of TypeScript's value--use `unknown` instead
- Overusing `as` to "make the error go away" (it often hides real bugs)
- Widening types accidentally (forgetting `as const` / losing literal types)
- Narrowing too late (doing work before you validate unknown input)
- Over-typing simple code instead of relying on inference
- Confusing `interface` vs `type` (pick one convention for objects)
- Ignoring strict mode flags like `strictNullChecks` and `noUncheckedIndexedAccess`
TypeScript FAQ
Interface or type alias?
Use `interface` for extendable object shapes (it supports declaration merging and extends). Use `type` for unions, primitives, and composition (e.g., `A | B`, `A & B`). In practice, either works for objects--pick one convention and be consistent.
unknown vs any?
`unknown` forces you to narrow before use; `any` disables safety entirely. Prefer `unknown` + narrowing for untrusted inputs (API responses, user data). `any` should be a last resort, not a convenience.
What's the difference between `as` and `satisfies`?
`as` can bypass type checks and lose precision. `satisfies` (TS 4.9+) validates the shape while keeping precise inferred types. Use `satisfies` when you want to check a value conforms to a type without widening it.
Why does `string | undefined` keep showing up?
TS is warning you about a missing case. Fix it by narrowing (`if (!x) return`), providing defaults (`x ?? 'fallback'`), or using optional chaining (`x?.property`).
What is type narrowing?
Narrowing is how TypeScript refines a broad type to a more specific one within a code block. Use `typeof`, `instanceof`, `in`, equality checks, or type guard functions to narrow.
What is a type guard function?
A function with return type `x is SomeType`. When it returns true, TypeScript narrows the parameter to that type in subsequent code. Use it for complex validation logic.
When should I use generics?
When you want a function or type to work with any type while preserving type information. If you're writing `any` because you don't know the type ahead of time, you probably want a generic instead.
What does `extends` mean in generics?
It constrains the generic: `<T extends string>` means T must be assignable to string. Think of it as 'T must be at least this type.'
What are utility types?
Built-in types that transform other types. `Partial<T>` makes all properties optional. `Required<T>` makes them required. `Pick<T, K>` extracts keys. `Omit<T, K>` removes keys. `Record<K, V>` creates an object type.
What is a discriminated union?
A union of types that share a common literal property (the discriminant). TypeScript uses this property to narrow the union exhaustively. Common pattern: `{ status: "success"; data: T } | { status: "error"; error: Error }`.
How do I handle exhaustive checking?
Use the `never` type. If a switch/if-else should handle all cases, assign the value to `never` in the default--if you miss a case, TypeScript will error because that case isn't assignable to `never`.
Why does TypeScript complain about index signatures?
Accessing `obj[key]` returns `T | undefined` with `noUncheckedIndexedAccess` (recommended). This catches bugs where the key might not exist. Use `in` checks or non-null assertion (sparingly) to handle it.
TypeScript Syntax Quick Reference
type User = { id: number; name: string };
function isUser(x: unknown): x is User {
if (typeof x !== 'object' || x === null) return false;
if (!('id' in x) || typeof x.id !== 'number') return false;
if (!('name' in x) || typeof x.name !== 'string') return false;
return true;
}type Result<T> =
| { status: 'success'; value: T }
| { status: 'error'; error: string };
function handle(r: Result<string>) {
if (r.status === 'success') {
console.log(r.value); // TypeScript knows value exists
} else {
console.log(r.error); // TypeScript knows error exists
}
}const routes = {
home: '/',
user: '/users/:id',
settings: '/settings',
} satisfies Record<string, string>;
// routes.home is still typed as '/' (literal), not stringfunction pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: 'Alice' };
const name = pluck(user, 'name'); // type: stringinterface User {
id: number;
name: string;
email: string;
}
type UserPreview = Pick<User, 'id' | 'name'>;
type UserUpdate = Partial<Omit<User, 'id'>>;
type UserLookup = Record<string, User>;type Status = 'pending' | 'active' | 'done';
function getLabel(status: Status): string {
switch (status) {
case 'pending': return 'Waiting...';
case 'active': return 'In Progress';
case 'done': return 'Complete';
default:
const _exhaustive: never = status;
return _exhaustive; // Error if case missed
}
}interface Config {
apiUrl: string;
timeout: number;
debug: boolean;
}
type ConfigKey = keyof Config; // 'apiUrl' | 'timeout' | 'debug'
type ConfigValue = Config[ConfigKey]; // string | number | booleanTypeScript Sample Exercises
Declare a variable `age` with type `number`
let age: number;Declare a variable `isActive` with type `boolean`
let isActive: boolean;Declare a constant `age` with type `number` and value `25`
const age: number = 25;+ 47 more exercises