Week 1: Modern JavaScript & TypeScript for Angular

Every Angular API you'll touch this course — signals, decorators, typed forms, generics in HttpClient — is TypeScript underneath. This week builds that language foundation properly, so nothing later feels like syntax you're guessing at.

Phase 1 of 7 Week 1 of 26 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Read and write idiomatic modern JavaScript/TypeScript without hesitation
  • Follow Angular's own source code using its type system
  • Configure a TypeScript project — and its strictness — with intent

1. ES2022+ Essentials

Angular templates and TypeScript code lean hard on a handful of modern JavaScript features. If any of the four below feel unfamiliar, that's exactly what this section fixes — you'll see all four in nearly every component you write from Week 4 onward.

Destructuring

Destructuring pulls values out of objects and arrays into named variables in one step. It's how you'll read @Input() data, route params, and API responses throughout the course.

Optional chaining & nullish coalescing

?. stops property access the moment it hits null or undefined, instead of throwing. ?? supplies a fallback — but only for null/undefined, not for every falsy value the way || does. That distinction matters: a quantity of 0 or an empty string '' should usually survive ??, but not ||.

example.ts
interface User {
  id: number;
  name: string;
  profile?: {
    avatarUrl?: string;
  };
}

function describeUser(user: User): string {
  const { name, profile } = user;
  const avatar = profile?.avatarUrl ?? '/assets/default-avatar.png';
  return `${name} → ${avatar}`;
}

// Array destructuring + rest
const [first, second, ...remaining] = [10, 20, 30, 40];

// Spread: merge objects without mutating either one
const merged = { ...{ role: 'admin' }, ...{ active: true } };
Why it matters for Angular

Signal-based inputs, route data, and reactive-form values are frequently optional. ?. and ?? are how you'll handle "this might not exist yet" without littering your templates in if checks.

2. Modules, Promises & Async/Await

Every Angular file is an ES module — one file, one set of import/export statements. And nearly every piece of data you'll show a user arrives asynchronously, via fetch, HttpClient, or an RxJS stream. async/await is the readable syntax sitting on top of Promises that makes that code look synchronous.

user.service.ts
export interface User {
  id: number;
  name: string;
}

export async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);

  if (!response.ok) {
    throw new Error(`Failed to load user ${id}`);
  }

  return response.json();
}
main.ts
import { fetchUser } from './user.service';

fetchUser(42)
  .then(user => console.log(user.name))
  .catch(err => console.error(err));

// or, inside an async function:
async function run() {
  try {
    const user = await fetchUser(42);
    console.log(user.name);
  } catch (err) {
    console.error(err);
  }
}

You won't write fetch calls directly once you reach Week 14 (HttpClient), but the async/await mental model transfers over exactly — Angular's HTTP layer just wraps it in an Observable instead of a Promise.

3. Types, Interfaces & Generics

TypeScript's type system is what turns a typo like usre.name into a red squiggle at compile time instead of a bug your users find. Interfaces describe the shape of an object; generics let a function or type work with whatever type you give it, without losing type safety.

api.ts
interface ApiResponse<T> {
  data: T;
  status: number;
}

function unwrap<T>(response: ApiResponse<T>): T {
  return response.data;
}

const usersResponse: ApiResponse<User[]> = { data: [], status: 200 };
const users = unwrap(usersResponse); // inferred as User[]

Notice you never told unwrap what T was — TypeScript inferred it from the argument. This is exactly how Angular's own APIs are typed: input<T>(), signal<T>() and HttpClient.get<T>() all use this same generic pattern you'll now recognize on sight.

4. Union Types, Narrowing & Discriminated Unions

A union type says "this value is one of these shapes." A discriminated union adds a shared, literal field (often called status or kind) that lets TypeScript figure out — narrow — exactly which shape you're holding at any point in your code.

load-state.ts
type LoadState =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: User[] }
  | { status: 'error'; message: string };

function renderState(state: LoadState): string {
  switch (state.status) {
    case 'idle':
      return 'Waiting to start…';
    case 'loading':
      return 'Loading users…';
    case 'success':
      // TypeScript knows `state.data` exists here — and only here
      return `Loaded ${state.data.length} users`;
    case 'error':
      return `Error: ${state.message}`;
  }
}
You'll see this shape constantly

The resource() API in Week 16 and every loading/error UI you build returns almost exactly this idle / loading / success / error union. Learning to model it now pays off for the rest of the course.

5. Classes & Decorators

Angular components are classes, and even in the standalone, signals-first Angular you're learning, classes are still tagged with decorators@Component, @Injectable — to attach metadata the framework reads before your class is ever instantiated. A decorator is just a function that runs on a class, method, or property at definition time.

logging.ts
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function (...args: unknown[]) {
    console.log(`Calling ${propertyKey} with`, args);
    return original.apply(this, args);
  };
}

class UserService {
  @Log
  getUser(id: number) {
    return { id, name: 'Ada Lovelace' };
  }
}

new UserService().getUser(42);
// logs: "Calling getUser with [42]"

You'll never write a decorator like @Log day to day — but seeing one built from scratch demystifies @Component({...}): it's not magic, it's a function that receives your class and attaches the config object as metadata Angular reads later.

6. tsconfig.json Deep Dive & Strict Mode

tsconfig.json controls how forgiving — or strict — the compiler is. The Angular CLI generates a strict config by default, and this course keeps it that way: strict mode catches entire categories of bugs (like forgetting a value can be null) before your code ever runs.

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "strict": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "angularCompilerOptions": {
    "strictTemplates": true
  }
}

strict: true is a bundle of flags, but two are worth knowing by name:

  • strictNullChecksnull and undefined are no longer silently assignable to every type, which is exactly why the ?./?? patterns from Section 1 exist.
  • noImplicitAny — every parameter needs a type, either explicit or inferred. No more silent any.

strictTemplates is Angular-specific: it applies the same rigor to expressions inside your HTML templates, so a typo in {{ user.nmae }} fails the build instead of silently rendering nothing.

7. Hands-on Exercise

Hands-on

Convert a vanilla-JS utility library to strict TypeScript

Below is a small, untyped JavaScript utility file. Convert it to TypeScript under strict mode, applying everything from this week.

string-utils.js — starter (untyped)
function truncate(str, max) {
  return str.length > max ? str.slice(0, max) + '…' : str;
}

function pluck(list, key) {
  return list.map(item => item[key]);
}

function safeDivide(a, b) {
  if (b === 0) return { ok: false, error: 'Division by zero' };
  return { ok: true, value: a / b };
}

Requirements:

  1. Create string-utils.ts and add explicit types to every parameter and return value — no implicit any.
  2. Make pluck generic: pluck<T, K extends keyof T>(list: T[], key: K): T[K][], so the return type is correctly inferred from whatever list you pass in.
  3. Model safeDivide's return value as a discriminated union Result<T> ({ ok: true; value: T } | { ok: false; error: string }) instead of an untyped object literal.
  4. Enable strict: true in a local tsconfig.json and fix every error the compiler reports.
  5. Write three quick call sites that prove the types work — including one that intentionally uses the result of safeDivide and narrows on ok before reading .value.
Hint

If pluck's generic constraint feels unfamiliar, start without it — type key as string — get everything else working, then come back and tighten it to K extends keyof T once the rest compiles.

8. Knowledge Check

Four quick questions. Expand each to check your answer — no grading, just a gut check before Week 2.

Q1

What's the actual difference between ?? and ||?

?? only falls back to its right-hand side when the left side is null or undefined. || falls back on any falsy value — including 0, '', and false. That difference matters whenever a legitimate value could be 0 or an empty string, like a quantity or a search query.

Q2

Why does Angular still use decorators like @Component if most new APIs are function-based?

Decorators attach metadata to a class itself — before any instance exists. Angular needs to know "this class is a component, here's its selector and template" ahead of time, which a function called inside the class body can't express. Functions like inject() or signal() replaced patterns that happened inside a class; decorators still describe the class as a whole.

Q3

What does strict: true in tsconfig.json actually turn on?

It's a bundle of individual flags — most notably strictNullChecks (forces you to handle null/undefined explicitly) and noImplicitAny (every value needs a real type). Also included: strictFunctionTypes, strictPropertyInitialization, strictBindCallApply, and a few more — all defaulted to their strictest setting.

Q4

Why is switching on a discriminant safer than an if/else chain that checks individual properties?

TypeScript narrows the type inside each case automatically, so you get autocomplete and type-checking on exactly the fields that variant has. It also enables exhaustiveness checking: if you add a new variant to the union later and forget to handle it in the switch, assigning the unhandled case to a never-typed variable will fail to compile — catching the gap immediately instead of at runtime.