Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. JavaScript
  3. TypeScript Practice
JavaScript50 exercises

TypeScript Practice

Practice TypeScript syntax with short drills: generics, narrowing unknown, utility types, discriminated unions, and fixing common compiler errors like TS2322, TS2339, and TS7006.

Common ErrorsQuick ReferencePractice
Warm-up1 / 2

Can you write this from memory?

Declare a variable `name` with type `string`

On this page
  1. 1TypeScript validates, you write the same JavaScript
  2. 2Interface vs type: the real answer
  3. 3Type narrowing: how TypeScript gets smarter
  4. Narrowing techniques
  5. Type guard functions
  6. 4Generics: reusable type-safe code
  7. Generic constraints
  8. The keyof + extends pattern
  9. 5Utility types: transforming types
  10. Essential utility types
  11. 6Discriminated unions: safe branching
  12. Exhaustive checking with never
  13. 7satisfies: check without widening
  14. 8Common compiler errors (and fixes)
  15. TS2322: Type 'X' is not assignable to type 'Y'
  16. TS2339: Property 'X' does not exist on type 'Y'
  17. TS18046: 'X' is of type 'unknown'
  18. TS2345: Argument of type 'X' is not assignable to parameter of type 'Y'
  19. TS7006: Parameter 'x' implicitly has an 'any' type
  20. 9Quick reference
  21. 10References
TypeScript validates, you write the same JavaScriptInterface vs type: the real answerType narrowing: how TypeScript gets smarterGenerics: reusable type-safe codeUtility types: transforming typesDiscriminated unions: safe branchingsatisfies: check without wideningCommon compiler errors (and fixes)Quick referenceReferences

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.

Related JavaScript Topics
JavaScript FunctionsJavaScript Modules & ScopeJavaScript React HooksJavaScript Classes

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.


Ready to practice?

Start practicing TypeScript with spaced repetition

Use interface for extendable objects and declaration merging. Use type for 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 caseRecommendation
Object shapeEither (be consistent)
Extending/inheritinginterface (cleaner syntax)
Declaration merginginterface (only option)
Unionstype (only option)
Primitives/tuplestype (only option)
Mapped typestype (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

TechniqueNarrows
typeof x === 'string'Primitives
x instanceof DateClass instances
'key' in objObjects with property
x === nullNull 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

UtilityDoesExample
Partial<T>All properties optionalForm state, updates
Required<T>All properties requiredValidated config
Pick<T, K>Keep only certain keysAPI responses
Omit<T, K>Remove certain keysHide internal fields
Record<K, V>Object with key type K, value type VLookup tables
Readonly<T>All properties readonlyImmutable 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.


PatternCode
Optional propertyname?: string
Union typestring | number
Intersection typeA & B
Type assertionvalue as Type (use sparingly)
Non-null assertionvalue! (use sparingly)
Type guardfunction isX(x): x is X { ... }
Generic functionfunction f<T>(x: T): T { ... }
Generic constraint<T extends SomeType>
Indexed accessT[K]
keyofkeyof T (union of keys)
satisfiesvalue 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

Prompt

Define a User type and write a type guard that narrows unknown input to User.

What a strong answer looks like

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

Type annotations (params, returns, variables) and inferenceUnion types, optional properties, and strict null handlingType narrowing: `typeof`, `in`, equality checks, predicatesInterfaces, type aliases, and composition (`&`, `|`)Generics: constraints (`extends`), defaults, inferenceUtility types: Partial, Required, Pick, Omit, RecordKey operators: `keyof`, `typeof`, indexed access (`T[K]`)Discriminated unions + exhaustive checking`satisfies` for safe shape checks without type-wideningReading common compiler errors (TS2322, TS2339, TS7006, TS18046)

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 Guard (narrow unknown)
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;
}
Discriminated Union (safe branching)
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
  }
}
`satisfies` (check shape, keep literal types)
const routes = {
  home: '/',
  user: '/users/:id',
  settings: '/settings',
} satisfies Record<string, string>;

// routes.home is still typed as '/' (literal), not string
Generic Constraint
function 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: string
Utility Types
interface User {
  id: number;
  name: string;
  email: string;
}

type UserPreview = Pick<User, 'id' | 'name'>;
type UserUpdate = Partial<Omit<User, 'id'>>;
type UserLookup = Record<string, User>;
Exhaustive Switch
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
  }
}
`keyof` and Indexed Access
interface Config {
  apiUrl: string;
  timeout: number;
  debug: boolean;
}

type ConfigKey = keyof Config; // 'apiUrl' | 'timeout' | 'debug'
type ConfigValue = Config[ConfigKey]; // string | number | boolean

TypeScript Sample Exercises

Example 1Difficulty: 1/5

Declare a variable `age` with type `number`

let age: number;
Example 2Difficulty: 1/5

Declare a variable `isActive` with type `boolean`

let isActive: boolean;
Example 3Difficulty: 1/5

Declare a constant `age` with type `number` and value `25`

const age: number = 25;

+ 47 more exercises

Start practicing TypeScript

Free daily exercises with spaced repetition. No credit card required.

← Back to JavaScript Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.