TypeScript

A quick reference for TypeScript — why it exists, type annotations, interfaces, DOM typing, and the non-null / type assertion operators.

Why TypeScript Exists

JavaScript doesn't lock a variable to one type — it can silently change. TypeScript adds type annotations so mismatches are caught before the code runs, at compile time, rather than discovered later as a runtime bug.

Problem 1 — silent type drift:

let x: number = 100;
// ... hundreds of lines later ...
x = 'HELLO';          // TS error: Type 'string' is not assignable to type 'number'

Without TypeScript, x silently becomes a string and x + y becomes
'HELLO5' instead of adding two numbers — a hard-to-trace bug in a
large codebase (imagine this happening to something like a user's SSN).
                    
Problem 2 — inconsistent return types:

function getUser(id) {
  if (found)  return { name: 'Alan' };   // object
  if (error)  return 'Not found';        // string
              return null;               // null
}

Whoever calls getUser() has no guarantee which type comes back.
TypeScript forces you to declare what a function returns — and flags it
immediately if the implementation doesn't match the declared type.
                    
Key facts:
- TypeScript compiles DOWN to plain JavaScript — the browser never
  sees or runs TypeScript directly.
- Originally pitched as "the future of JavaScript" — many features it
  introduced early have since become standard JS (ES6+).
- Upfront rigidity costs a little more development time but drastically
  cuts down on runtime bugs later.
                    

Compiler Setup

TypeScript is installed per project (not globally), so all compiler commands are run via npx. If a tsconfig.json exists in the project root (created during setup), running npx tsc picks it up automatically — no need to specify individual files.

Command Description
npx tsc Compile the project once using tsconfig.json.
npx tsc --watch Watch mode — recompiles automatically every time you save. No need to re-run manually.
Windows-specific setup notes:

- If you see "running scripts is disabled on this system" in PowerShell,
  adjust the execution policy with Set-ExecutionPolicy (exact command in
  the instructor's Discord setup doc).

- `touch` does not exist on Windows — use New-Item to create a file:
    New-Item index.ts
                    

Type Annotations

A type annotation locks a variable, parameter, or return value to a specific type. Mismatches are caught at compile time — before the code runs.

Syntax Description
let x: string = 'hello'; Locks x to only ever hold a string. Assigning a number later throws a compile-time error.
let x = 5; TypeScript INFERS the type as number automatically. Don't write the annotation — it's redundant when the type is already obvious.
let x: any; Escape hatch — opts out of type checking entirely. Useful in rare cases but defeats the purpose of TypeScript. Avoid relying on it.
let x: string = 'hello';
x = 44;
// Error: Type 'number' is not assignable to type 'string'.

// Don't annotate what TypeScript can already infer:
let x: number = 5;   // redundant — TS already knows it's a number from the value
let x = 5;           // correct — type is still number, less noise

Typed Arrays

Locks every item in an array to a specific type. Especially useful for arrays of objects — you get autocomplete and immediate type warnings while looping through them.

Syntax Description
string[] Shorthand — every item must be a string.
Array<string> Generic form — identical to string[], different syntax only.
(string | number)[] Union typed array — items can be a string OR a number.
Card[] Array of objects typed by an interface — every item must match the Card interface shape.
let x: string[] = [];
x = [1];
// Error: Type 'number' is not assignable to type 'string'.

const suite: Suite[] = ['❤️', '♠️', '♣️', '♦️'];
const value: (string | number)[] = [2, 3, 4, 5, 6, 7, 8, 9, 10, 'J', 'Q', 'K', 'A'];
let player1: Card[] = [];
Note: accessing an array by index (e.g. deck[5]) returns T | undefined
in strict TypeScript — the element might not exist at that position.
This is why array swaps and .pop() often need the non-null assertion !
(see the Non-null Assertion section below).
                    

Interfaces

An interface defines the required shape of an object — what properties it must have, and what type each one is. Think of it as a written contract: it removes all ambiguity about what's required.

interface Card {
    value: number | string;
    suite: Suite;
}

interface Deck {
    deckOfCards: Card[];
}
Key rules:

- Accessing a property NOT in the interface (e.g. card.cat when Card
  only has value/suite) throws an immediate, clear error — TypeScript
  tells you exactly which property doesn't exist on that type.
- Your editor autocompletes ONLY the valid properties once it knows
  the type — no guessing what's available.
- "Always pass around the interface, not the implementation" — type
  your function parameters using the interface, not a loose untyped
  object. This guarantees callers can't pass in the wrong shape:

    function greet(human: Human) { ... }   // correct — enforced contract
    function greet(human: object) { ... }  // too loose — no protection
                    
// implements — a class formally declares it fulfills an interface's contract:
class Card implements Card {
    constructor(value: number | string, suite: Suite) {
        this.value = value;
        this.suite = suite;
    }
}

// If a required property is missing from the class, TypeScript throws
// a clear error telling you exactly what's missing and where.

Union & Custom Types

Syntax Description
number | string Union type — the value can be EITHER a number OR a string. Useful when a property genuinely has multiple valid types (e.g. card values: 2–10 are numbers, J/Q/K/A are strings).
type Suite = 'Hearts' | 'Diamonds' Custom literal type — restricts the value to only these exact string options. Any other string (including wrong capitalisation) throws a compile-time error.
type Suite = '❤️' | '♠️' | '♣️' | '♦️';

interface Card {
    value: number | string;
    suite: Suite;
}
Capitalisation trap — custom literal types are EXACT string matches:

type Suite = 'Hearts' | 'Diamonds' | 'Clubs' | 'Spades';

let s: Suite = 'hearts';
// Error: Type '"hearts"' is not assignable to type 'Suite'.
// The expected type comes from property 'suite' which is declared
// here on type 'Card'.

TypeScript's error messages for custom types are genuinely readable —
they tell you exactly which property and which type caused the mismatch.
A common late-night debugging trap: everything looks right until you
spot the wrong capitalisation.
                    

Functions

The same annotation pattern applies to function parameters and return values — annotate each parameter after its name, and the return type after the closing ), before the {.

function doMath(num1: number, num2: number): number {
    return num1 + num2;
}

function dealCards(numberOfCards: number): void {
    // void — this function doesn't return anything
}
Return type Description
: number Function must return a number. TypeScript flags it immediately if the actual return value doesn't match.
: void Function returns nothing — used for event listener callbacks and functions that only cause a side effect.
: Card | undefined Function may return a Card OR undefined. Callers must handle both cases (or use ! to assert it's always defined).
Typing the return value catches mismatches — if your function is typed
: number but accidentally returns a string on one code path, TypeScript
flags it before you run anything.

drawCard() returns Card | undefined because it uses .pop() internally,
which always returns T | undefined (array might be empty). The return
type follows the implementation — TypeScript infers it for you.
                    

Non-null Assertion (!)

The ! operator tells TypeScript: "I know this won't be null or undefined here — trust me." Use it when TypeScript can't prove it, but you can.

Where it appears Why it's needed
element!.addEventListener(...) getElementsByClassName('x')[0] returns Element | undefined — the element at that index might not exist. ! asserts it does.
this.drawCard()! .pop() returns T | undefined (array might be empty). Any function wrapping .pop() inherits that type. ! asserts a value will always come back.
this.deckOfCards[i]! Array index access returns Card | undefined in strict mode. ! asserts the element exists at that index.
// Array swap in shuffleBot() — both sides need ! because index access
// returns Card | undefined:
let tempCard = this.deckOfCards[i];
this.deckOfCards[i] = this.deckOfCards[randomCard]!;
this.deckOfCards[randomCard] = tempCard!;

// .pop() returns Card | undefined — ! asserts it's always a Card here:
let deal = this.drawCard()!;
player1.push(deal);

// DOM element from getElementsByClassName — ! at point of use:
const addPlayer = document.getElementsByClassName('add')[0];
addPlayer!.addEventListener('click', handler);
Tradeoff: ! removes the TypeScript safety check at that point. If the
value IS null/undefined at runtime, you get a regular JS crash instead
of a helpful TypeScript error.

Only use it when you're genuinely certain the value exists — like a DOM
element you know is in the HTML, or an array swap where both indices are
within bounds.
                    

Type Assertion (as)

The as keyword narrows a value to a more specific type when TypeScript can't infer it automatically. Most common with DOM queries, where TypeScript only knows the broad type (HTMLElement) but you need the specific one (HTMLInputElement) to access properties like .value.

// getElementById returns HTMLElement | null — too broad to access .value:
const display = document.getElementById('playersDisplay');
display.value;   // Error: Property 'value' does not exist on type 'HTMLElement'

// as HTMLInputElement narrows the type AND removes null in one step:
const display = document.getElementById('playersDisplay') as HTMLInputElement;
display.value;          // fine — HTMLInputElement has .value
display.valueAsNumber;  // fine — also specific to HTMLInputElement
HTML element TypeScript type (as ...)
<input> HTMLInputElement
<button> HTMLButtonElement
<div> HTMLDivElement
<select> HTMLSelectElement
<textarea> HTMLTextAreaElement
How to find the right type — MDN workflow:

1. Go to the MDN page for the HTML element (e.g. search "MDN input element")
2. Scroll to the Technical Summary section
3. Look at the DOM Interface row — that's the TypeScript type to use in `as`

as vs !:
- `as` narrows the TYPE (e.g. HTMLElement → HTMLInputElement) and also
  removes null when the narrowed type doesn't include it.
  Use this with getElementById when you need the specific element type.

- `!` only removes null/undefined — it doesn't change the type.
  Use this with getElementsByClassName[0] where the return type is
  already Element, but the index access may be undefined.