Typescript
TypeScript Tutorial Notes
Course: TypeScript for Beginners
Instructor: Dave Gray
Total Duration: 8+ hours (17 tutorials)
Chapter 1: Start Here
What is TypeScript?
TypeScript is JavaScript with syntax for types. It's a strongly typed programming language that builds on
JavaScript, providing better tooling at any scale.
Key Points:
TypeScript is a developer tool that helps write better JavaScript
Created by Microsoft (Anders Heisberg also created C#)
TypeScript compiles to JavaScript
Ranked 5th in Stack Overflow's most popular programming languages
Prerequisites
Before learning TypeScript, you should know JavaScript fundamentals, as TypeScript is a superset of
JavaScript and extends it.
Required Tools
1. Visual Studio Code - [Link]
2. [Link] (includes npm) - [Link]
Setup & Installation
# Install TypeScript globally
npm install typescript -g
# Initialize TypeScript config
tsc --init
# Compile a TypeScript file
tsc [Link]
# Watch mode for a single file
tsc [Link] -w
# Watch all files (with config)
tsc -w
Project Structure
project/
├── src/ # TypeScript source files
├── build/ # Compiled output
│ ├── [Link]
│ ├── css/
│ └── js/ # Compiled JavaScript
└── [Link] # TypeScript configuration
TypeScript Configuration ([Link])
Important settings:
{
"compilerOptions": {
"target": "ES2016", // JavaScript version to compile to
"rootDir": "./src", // Source directory
"outDir": "./build/js", // Output directory
"noEmitOnError": true // Don't compile if errors exist
},
"include": ["src"] // Only compile files in src
}
Compilation Behavior
TypeScript compiles to JavaScript even with errors (by default)
Use noEmitOnError: true to prevent compilation when errors exist
Can also use flag: tsc --noEmitOnError -w
ES5 uses var , ES2016+ uses let/const
Key Concepts
Strongly Typed vs Loosely Typed:
Strongly typed: Requires type specification (TypeScript)
Loosely typed: No type specification required (JavaScript)
Static vs Dynamic Typing:
Static: Types checked at compile time (TypeScript)
Dynamic: Types checked at runtime (JavaScript)
TypeScript Benefits:
Self-documenting code
Catches errors during development
Great for teams
Better tooling and IntelliSense
Chapter 2: Basic Types
Terminology
Inference: TypeScript figures out the type automatically
Implicit: Type is not explicitly stated but inferred
Explicit: Type is directly declared
Basic Type Syntax
// Implicit (TypeScript infers the type)
let myName = "Dave"; // inferred as string
// Explicit (TypeScript told the type)
let myName: string = "Dave";
// Variable without assignment
let myName: string;
myName = "Dave"; // Must be a string
Primitive Types
// String
let myName: string = "Dave";
// Number
let meaningOfLife: number = 42;
// Boolean
let isLoading: boolean = true;
// Any (allows any type - use sparingly!)
let album: any = "Van Halen";
album = 1984; // OK
album = true; // OK
Union Types
Union types allow a variable to be one of several types:
let album: string | number = "Van Halen";
album = 1984; // OK
album = true; // Error!
let postId: string | number;
let isActive: number | boolean;
// Union types can have more than 2 types
let value: string | number | boolean;
Regular Expression Type
let re: RegExp = /\w+/g;
Functions and Types
// Function with typed parameters
function sum(a: number, b: number) {
return a + b; // TypeScript infers return type: number
}
// Mixed types
function concat(a: number, b: string) {
return a + b; // TypeScript infers return type: string
}
Type Safety Benefits
TypeScript prevents common JavaScript errors:
let myName: string = "Dave";
myName = 42; // Error: Type 'number' is not assignable to type 'string'
// Variable reassignment is allowed with same type
myName = "John"; // OK
The any Type
The any type defeats TypeScript's purpose but has valid use cases:
let album: any;
album = "Van Halen"; // OK
album = 1984; // OK
album = true; // OK
When to use any :
When you're unsure what type you'll receive
When migrating JavaScript to TypeScript
Use sparingly - it defeats type safety
Important Notes
Valid JavaScript is valid TypeScript
TypeScript compiler may warn but still compiles
Inference is OK, but explicit types are clearer
Union types provide flexibility with safety
Chapter 3: Arrays & Objects
Arrays
Basic Array Types
// String array (inferred)
let stringArr = ["hey", "hello", "Dave"];
// Type: string[]
// Union type array (inferred)
let guitars = ["Strat", "Les Paul", 5150];
// Type: (string | number)[]
// Multiple types (inferred)
let mixedData = ["EVH", 1984, true];
// Type: (string | number | boolean)[]
Explicit Array Types
// Empty array with explicit type
let bands: string[] = [];
[Link]("Van Halen"); // OK
[Link](42); // Error!
// Empty array without type becomes 'any'
let test = []; // Type: any[]
Array Operations
let stringArr = ["one", "hey", "Dave"];
// Reassignment
stringArr[0] = "John"; // OK
stringArr[0] = 42; // Error!
// Adding elements
[Link]("hey"); // OK
[Link](42); // Error!
// Union type arrays
let guitars: (string | number)[] = ["Strat", "Les Paul", 5150];
guitars[0] = 1984; // OK (can swap types)
[Link]("Jim"); // OK
[Link](true); // Error!
Array Assignment Rules
let stringArr: string[] = ["one", "two"];
let guitars: (string | number)[] = ["Strat", 5150];
let mixedData: (string | number | boolean)[] = ["EVH", 1984, true];
stringArr = guitars; // Error! (guitars accepts numbers)
guitars = stringArr; // OK (guitars accepts strings)
guitars = mixedData; // Error! (guitars doesn't accept boolean)
mixedData = guitars; // OK (mixedData accepts string & number)
Tuples
Tuples are arrays with fixed length and specific types in specific positions:
// Tuple definition
let myTuple: [string, number, boolean] = ["Dave", 42, true];
// Regular array (for comparison)
let mixed = ["John", 1, false]; // Type: (string | number | boolean)[]
// Key differences
mixed = myTuple; // OK (tuple can assign to array)
myTuple = mixed; // Error! (array might not have exactly 3 elements)
// Tuples enforce position types
myTuple[0] = "John"; // OK
myTuple[0] = 42; // Error! (position 0 must be string)
myTuple[3] = 42; // Error! (only 3 elements allowed)
Objects
Basic Object Types
// Simple object type annotation
let myObject: object;
// Arrays are also objects!
myObject = []; // OK
myObject = bands; // OK
myObject = {}; // OK
[Link](typeof myObject); // "object"
Object with Properties
// Object with inferred property types
const exampleObj = {
prop1: "Dave",
prop2: true
};
// TypeScript infers:
// prop1: string
// prop2: boolean
exampleObj.prop1 = "John"; // OK
exampleObj.prop1 = 42; // Error!
exampleObj.prop2 = false; // OK
Type Aliases for Objects
Using type keyword to define object structure:
type Guitarist = {
name: string;
active: boolean;
albums: (string | number)[];
};
let evh: Guitarist = {
name: "Eddie",
active: false,
albums: [1984, 5150, "OU812"]
};
let jp: Guitarist = {
name: "Jimmy",
active: true,
albums: ["I", "II", "IV"]
};
// Can reassign if both are same type
evh = jp; // OK (both are Guitarist type)
Optional Properties
type Guitarist = {
name: string;
active?: boolean; // Optional property
albums: (string | number)[];
};
// Now 'active' is optional
let jp: Guitarist = {
name: "Jimmy",
albums: ["I", "II", "IV"]
// No 'active' property - OK!
};
Type Narrowing
When a property is optional, TypeScript requires checking before use:
type Guitarist = {
name?: string;
active: boolean;
albums: (string | number)[];
};
function greetGuitarist(guitarist: Guitarist) {
// Error without narrowing:
// return `Hello ${[Link]()}!`;
// Correct with narrowing:
if ([Link]) {
return `Hello ${[Link]()}!`;
}
return "Hello!";
}
Interfaces vs Types
Both can define object shapes, but interfaces are commonly used for objects:
// Using 'type'
type Guitarist = {
name: string;
active: boolean;
albums: (string | number)[];
};
// Using 'interface' (works the same for basic cases)
interface Guitarist {
name: string;
active: boolean;
albums: (string | number)[];
}
When to use which:
Interface: Typically for class-like structures and objects
Type: For unions, primitives, and more complex types
For beginners: It's mostly preference for simple object definitions
Object Restrictions
type Guitarist = {
name: string;
active: boolean;
albums: (string | number)[];
};
let evh: Guitarist = {
name: "Eddie",
active: false,
albums: [1984, 5150]
};
// Cannot add properties not in type definition
[Link] = 40; // Error! Property 'years' doesn't exist
Enums
Enums are a TypeScript addition to JavaScript (not just a type-level feature):
// Basic enum
enum Grade {
U, // 0
D, // 1
C, // 2
B, // 3
A // 4
}
[Link](Grade.U); // 0
[Link](Grade.B); // 3
[Link](Grade.A); // 4
Custom Enum Values
// Start at 1 instead of 0
enum Grade {
U = 1, // 1
D, // 2
C, // 3
B, // 4
A // 5
}
[Link](Grade.U); // 1
[Link](Grade.B); // 4
[Link](Grade.A); // 5
Note: Unlike most TypeScript features, enums add something to JavaScript at runtime, not just compile
time.
Quick Reference
Type Annotations
// Variables
let name: string = "Dave";
let age: number = 42;
let isActive: boolean = true;
let data: any = "anything";
// Arrays
let names: string[] = ["Dave", "John"];
let mixed: (string | number)[] = ["Dave", 42];
// Tuples
let tuple: [string, number] = ["Dave", 42];
// Objects
let person: { name: string; age: number } = {
name: "Dave",
age: 42
};
// Type aliases
type Person = {
name: string;
age: number;
};
// Interfaces
interface Person {
name: string;
age: number;
}
// Enums
enum Status {
Active,
Inactive,
Pending
}
Common Patterns
// Optional properties
type User = {
name: string;
email?: string; // Optional
};
// Union types
let id: string | number;
// Type narrowing
if (typeof value === "string") {
// TypeScript knows 'value' is string here
}
// Function with types
function add(a: number, b: number): number {
return a + b;
}
Key Takeaways
1. TypeScript extends JavaScript - All valid JavaScript is valid TypeScript
2. Type inference is powerful - TypeScript can figure out types automatically
3. Explicit types are clearer - But inference works well too
4. Union types provide flexibility - Allow multiple types while maintaining safety
5. Tuples are strict arrays - Fixed length and specific position types
6. Objects need structure - Use type or interface to define shapes
7. Optional properties require narrowing - Check existence before use
8. Enums are runtime additions - Unlike most TypeScript features
Resources
Official TypeScript website: [Link]
Visual Studio Code: [Link]
[Link]: [Link]
Course GitHub resources: [link in video description]
Dave Gray's YouTube channel: @DaveGrayTeachesCode
Next: Chapter 4 and beyond will cover more advanced TypeScript concepts!
TypeScript Tutorial Notes - Part 2
Course: TypeScript for Beginners (Chapters 4-9)
Instructor: Dave Gray
Chapter 4: Functions
Type Aliases
Type aliases allow you to create custom names for any type, making code more readable and DRY
(Don't Repeat Yourself).
// Basic type alias
type StringOrNumber = string | number;
// Array type alias
type StringOrNumberArray = (string | number)[];
// Using aliases in other aliases
type UserId = StringOrNumber;
// Type aliases work with any TypeScript type
type Album = string | number;
Type vs Interface:
Type aliases: Can represent ANY TypeScript type (primitives, unions, etc.)
Interfaces: Best for objects and class-like structures
Cannot use interfaces for simple type aliases like union types
// This works with type
type PostId = string | number;
// This does NOT work with interface
interface PostId = string | number; // ❌ Error!
Literal Types
Literal types allow you to specify exact values a variable can have:
// Single literal (like const)
let myName: "Dave" = "Dave";
myName = "John"; // ❌ Error! Can only be "Dave"
// Useful with union types
type Username = "Dave" | "John" | "Amy";
let username: Username;
username = "Amy"; // ✅ OK
username = "Rachel"; // ❌ Error!
// Works with numbers too
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
Benefits:
Restrict values to specific options
Great for configuration or state management
Keep code DRY when used in multiple places
TypeScript provides IntelliSense for literal options
Functions
Basic Function Typing
// Explicit parameter and return types
function add(a: number, b: number): number {
return a + b;
}
// Arrow function
const subtract = (c: number, d: number): number => {
return c - d;
};
Void Return Type
Functions that don't return a value use void :
function logMessage(message: any): void {
[Link](message);
}
Function Type Signatures
Using type aliases for function signatures:
// Type alias for function signature
type MathFunction = (a: number, b: number) => number;
// Using the type
let multiply: MathFunction = function(c, d) {
return c * d;
};
// TypeScript infers c and d are numbers
Interface alternative (less common for functions):
interface MathFunction {
(a: number, b: number): number;
}
Optional Parameters
function addAll(a: number, b: number, c?: number): number {
if (typeof c !== "undefined") {
return a + b + c;
}
return a + b;
}
addAll(2, 3); // ✅ OK - returns 5
addAll(2, 3, 2); // ✅ OK - returns 7
Rules:
Optional parameters must come last
Use type guards to check if optional parameter exists
Default Parameters
function sumAll(a: number = 10, b: number, c: number = 2): number {
return a + b + c;
}
sumAll(2, 3); // 2 + 3 + 2 = 7
sumAll(undefined, 3); // 10 + 3 + 2 = 15
Important notes:
Default params can be anywhere in the parameter list
To skip a default param, pass undefined
Cannot use default values in function type signatures
Rest Parameters
function total(a: number, ...nums: number[]): number {
return a + [Link]((prev, curr) => prev + curr, 0);
}
total(10, 2, 3); // 15
total(1, 2, 3); // 6
Rules:
Rest parameters must be last
Represented as an array inside the function
Don't use array brackets when calling
The never Type
The never type represents values that never occur:
// Function that throws error
function createError(errorMsg: string): never {
throw new Error(errorMsg);
}
// Endless loop
const infinite = (): never => {
let i: number = 1;
while (true) {
i++;
}
};
When never appears:
Functions that explicitly throw errors
Functions with infinite loops
Unreachable code
Useful with type guards:
function createNumberOrString(value: number | string): string {
if (typeof value === "string") {
return "string";
}
if (typeof value === "number") {
return "number";
}
// This should never happen
return createError("This should never happen!");
}
Custom Type Guards
const isNumber = (value: any): boolean => {
return typeof value === "number" ? true : false;
};
function numberOrString(value: number | string): string {
if (isNumber(value)) {
return "number";
}
if (typeof value === "string") {
return "string";
}
return createError("This should never happen!");
}
Chapter 5: Type Assertions (Type Casting)
What are Type Assertions?
Type assertions tell TypeScript you know more about a type than it does. Sometimes called "type
casting" (though not the same as runtime casting in other languages).
Basic Assertion Syntax
Two ways to assert types:
// 1. 'as' keyword (preferred, works in TSX)
let myValue = someValue as string;
// 2. Angle bracket syntax (doesn't work in React TSX files)
let myValue = <string>someValue;
Converting Specificity
type One = string;
type Two = string | number;
type Three = "hello";
let a: One = "hello";
let b = a as Two; // Less specific (string → string | number)
let c = a as Three; // More specific (string → "hello")
Practical Use: Narrowing Types
function addOrConcat(
a: number,
b: number,
c: "add" | "concat"
): number | string {
if (c === "add") return a + b;
return "" + a + b;
}
// Assertion tells TypeScript we know the return type
let myVal: string = addOrConcat(2, 2, "concat") as string;
// ⚠️ Be careful - TypeScript trusts you!
let nextVal: number = addOrConcat(2, 2, "concat") as number; // Wrong but no
error!
The unknown Type
unknown is safer than any - you must narrow it before use:
// Double assertion (force casting)
let value = 10 as unknown as string; // Overrides TypeScript
When to use: Rarely! Only when absolutely necessary.
DOM Assertions
TypeScript doesn't know about your HTML, so assertions are common with DOM:
// TypeScript infers: HTMLImageElement | null
const img = [Link]("img");
// TypeScript infers: Element | null
const myImg = [Link]("img");
// Assertion to specific type
const img = [Link]("img") as HTMLImageElement;
const myImg = [Link]("img") as HTMLImageElement;
Non-null Assertion
Use ! to tell TypeScript a value is not null:
const img = [Link]("img")!; // Not null
[Link]; // OK
// Or use 'as' assertion
const myImg = [Link]("img") as HTMLImageElement;
[Link]; // OK
Note: Non-null assertion is redundant when using as with specific type.
Common DOM Element Types
HTMLImageElement
HTMLSpanElement
HTMLDivElement
HTMLElement
Element
Practical Example: Copyright Year
// JavaScript that needs TypeScript refactoring
const year = [Link]("year");
const thisYear = new Date().getFullYear();
[Link]("datetime", thisYear);
[Link] = thisYear;
Problems:
1. year is possibly null
2. thisYear is number but needs to be string
TypeScript solution:
const year: HTMLSpanElement | null = [Link]("year") as
HTMLSpanElement;
const thisYear: string = new Date().getFullYear().toString();
if (year) {
[Link]("datetime", thisYear);
[Link] = thisYear;
}
Better solution with assertions:
const year = [Link]("year") as HTMLSpanElement;
const thisYear: string = new Date().getFullYear().toString();
[Link]("datetime", thisYear);
[Link] = thisYear;
Key Takeaways
Assertions tell TypeScript "trust me, I know better"
TypeScript can't always check assertions - mistakes are possible
Use as syntax (works in React), avoid <> syntax in TSX
Common with DOM manipulation
Use sparingly - defeats TypeScript's safety
Chapter 6: Classes
Basic Class Structure
class Coder {
name: string;
music: string;
age: number;
lang: string;
constructor(name: string, music: string, age: number, lang: string) {
[Link] = name;
[Link] = music;
[Link] = age;
[Link] = lang;
}
}
Important: Properties must be declared both in class body AND constructor parameters.
Visibility Modifiers (Data Modifiers)
Three visibility modifiers control access:
class Coder {
public name: string; // Accessible everywhere (default)
private age: number; // Only within this class
protected lang: string; // Within class and subclasses
constructor(
name: string,
age: number,
lang: string
) {
[Link] = name;
[Link] = age;
[Link] = lang;
}
}
Shorthand with Visibility Modifiers
Using visibility modifiers in constructor removes redundancy:
class Coder {
constructor(
public name: string,
public music: string,
private age: number,
protected lang: string = "TypeScript" // Default value
) {}
}
const dave = new Coder("Dave", "Rock", 42);
Benefits:
Less repetitive code
Automatically creates and assigns properties
Can combine with readonly
Readonly Properties
class Coder {
constructor(
public readonly name: string,
public music: string,
private age: number
) {}
}
const dave = new Coder("Dave", "Rock", 42);
[Link] = "John"; // ❌ Error! Readonly property
[Link] = "Jazz"; // ✅ OK
Methods
class Coder {
constructor(
public name: string,
private age: number
) {}
public getAge(): string {
return `Hello, I'm ${[Link]}`;
}
}
const dave = new Coder("Dave", 42);
[Link]([Link]()); // "Hello, I'm 42"
[Link]([Link]); // ❌ Error! Private property
Extending Classes (Inheritance)
class WebDev extends Coder {
constructor(
public computer: string,
name: string,
age: number
) {
super(name, age); // Must call super() first!
}
public getLang(): string {
return `I write ${[Link]}`; // Access protected property
}
}
const sarah = new WebDev("Mac", "Sarah", 25);
Rules:
super() must be called before accessing this
Private properties from parent are NOT accessible
Protected properties ARE accessible in subclasses
Implementing Interfaces
interface Musician {
name: string;
instrument: string;
play(action: string): string;
}
class Guitarist implements Musician {
name: string;
instrument: string;
constructor(name: string, instrument: string) {
[Link] = name;
[Link] = instrument;
}
play(action: string): string {
return `${[Link]} ${action} the ${[Link]}`;
}
}
const page = new Guitarist("Jimmy", "guitar");
[Link]([Link]("strums")); // "Jimmy strums the guitar"
Important: All properties and methods from interface must be implemented.
Static Members
Static members belong to the class itself, not instances:
class Peeps {
static count: number = 0;
static getCount(): number {
return [Link]; // Access with class name, not 'this'
}
public id: number;
constructor(public name: string) {
[Link] = ++[Link]; // Increment class-level count
}
}
const john = new Peeps("John");
const steve = new Peeps("Steve");
const amy = new Peeps("Amy");
[Link]([Link]); // 3
[Link]([Link]); // 1
[Link]([Link]); // 2
[Link]([Link]); // 3
Getters and Setters
class Bands {
private dataState: string[];
constructor() {
[Link] = [];
}
public get data(): string[] {
return [Link];
}
public set data(value: string[]) {
if ([Link](value) && [Link](el => typeof el === "string")) {
[Link] = value;
return;
}
throw new Error("Param is not an array of strings");
}
}
const myBands = new Bands();
[Link] = ["Neil Young", "Led Zep"];
[Link]([Link]); // ["Neil Young", "Led Zep"]
[Link] = [...[Link], "ZZ Top"];
[Link]([Link]); // ["Neil Young", "Led Zep", "ZZ Top"]
Important:
Getters can have return types
Setters CANNOT have return types (not even void )
Use getters/setters to add validation logic
Chapter 7: Index Signatures & keyof Assertions
What are Index Signatures?
Index signatures are useful when:
1. You don't know exact property names in advance
2. You need to access object properties dynamically
The Problem
interface TransactionObj {
pizza: number;
books: number;
job: number;
}
const todaysTransactions: TransactionObj = {
pizza: -10,
books: -5,
job: 50
};
// Static access - OK
[Link]([Link]); // -10
// Dynamic access - ERROR!
let prop: string = "pizza";
[Link](todaysTransactions[prop]); // ❌ Error!
Error: "Element implicitly has an 'any' type because expression of type 'string' can't be used to index
type 'TransactionObj'"
Index Signature Solution
interface TransactionObj {
[index: string]: number; // Index signature
}
// Now dynamic access works
let prop: string = "pizza";
[Link](todaysTransactions[prop]); // ✅ OK
// Loop access also works
for (const transaction in todaysTransactions) {
[Link](todaysTransactions[transaction]); // ✅ OK
}
Index Signature Syntax
interface MyInterface {
[index: string]: number; // or 'key' instead of 'index'
// Keys are strings, values are numbers
}
Key types allowed:
string
number
symbol
Template literal types
NOT allowed: boolean
With Specific Properties
You can combine index signatures with specific required properties:
interface TransactionObj {
[index: string]: number;
pizza: number; // Required
books: number; // Required
job: number; // Required
}
const todaysTransactions: TransactionObj = {
pizza: -10,
books: -5,
job: 50,
dave: 42 // ✅ OK - additional property allowed
};
Readonly Index Signatures
interface TransactionObj {
readonly [index: string]: number;
}
const todaysTransactions: TransactionObj = {
pizza: -10
};
[Link] = 40; // ❌ Error! Index signature is readonly
Limitations
interface TransactionObj {
[index: string]: number;
}
const todaysTransactions: TransactionObj = {
pizza: -10
};
// TypeScript can't prevent accessing non-existent properties
[Link]([Link]); // undefined (no error!)
The keyof Assertion
When you don't have an index signature, use keyof to access properties dynamically:
interface Student {
name: string;
GPA: number;
classes?: number[];
}
const student: Student = {
name: "Doug",
GPA: 3.5,
classes: [100, 200]
};
// Without keyof - ERROR
for (const key in student) {
[Link](student[key]); // ❌ Error!
}
// With keyof assertion - OK
for (const key in student) {
[Link](student[key as keyof Student]); // ✅ OK
}
// Alternative with [Link]
[Link](student).map(key => {
[Link](student[key as keyof typeof student]);
});
keyof with Functions
function logStudentKey(student: Student, key: keyof Student): void {
[Link](`Student ${key}: ${student[key]}`);
}
logStudentKey(student, "name"); // IntelliSense shows: "name" | "GPA" | "classes"
logStudentKey(student, "GPA"); // ✅ OK
Record Utility Type (Alternative to Index Signatures)
// Instead of this:
interface Incomes {
[index: string]: number;
}
// Use Record:
type Streams = "salary" | "bonus" | "sideHustle";
type Incomes = Record<Streams, number>;
const monthlyIncomes: Incomes = {
salary: 500,
bonus: 100,
sideHustle: 250
};
Benefits:
Can use string literal types as keys
More specific than generic string index
Limitations:
Still requires keyof assertion for dynamic access
Can't specify different types for different properties
// Loop still needs assertion
for (const revenue in monthlyIncomes) {
[Link](monthlyIncomes[revenue as keyof Incomes]);
}
Comparison: Index Signature vs Record
Index Signature:
interface Student {
[key: string]: string | number | number[] | undefined;
name: string;
GPA: number;
classes?: number[];
}
Record Type:
type Streams = "salary" | "bonus" | "sideHustle";
type Incomes = Record<Streams, number>;
Chapter 8: Generics
Why Generics?
TypeScript is about strict types, but sometimes we don't know what types will be passed to:
Functions
Interfaces
Type aliases
Classes
Generics provide type variables - placeholders for types that will be specified later.
Basic Generic Function
// Non-generic - only works with strings
function stringEcho(arg: string): string {
return arg;
}
// Generic - works with any type
function echo<T>(arg: T): T {
return arg;
}
Convention: Use T for "Type" (but any name works)
Generic Utility Function Example
const isObj = <T>(arg: T): boolean => {
return (
typeof arg === "object" &&
 &&
arg !== null
);
};
[Link](isObj(true)); // false
[Link](isObj("John")); // false
[Link](isObj([1, 2, 3])); // false
[Link](isObj({ name: "John" })); // true
[Link](isObj(null)); // false
Generics with Complex Logic
const isTrue = <T>(arg: T): { arg: T; is: boolean } => {
if ([Link](arg) && ![Link]) {
return { arg, is: false };
}
if (isObj(arg) && .length) {
return { arg, is: false };
}
return { arg, is: !!arg }; // Double bang converts to boolean
};
[Link](isTrue(false)); // { arg: false, is: false }
[Link](isTrue(0)); // { arg: 0, is: false }
[Link](isTrue(true)); // { arg: true, is: true }
[Link](isTrue(1)); // { arg: 1, is: true }
[Link](isTrue("Dave")); // { arg: "Dave", is: true }
[Link](isTrue("")); // { arg: "", is: false }
[Link](isTrue(null)); // { arg: null, is: false }
[Link](isTrue({})); // { arg: {}, is: false }
[Link](isTrue({ name: "Dave" })); // { arg: {...}, is: true }
[Link](isTrue([])); // { arg: [], is: false }
[Link](isTrue([1, 2])); // { arg: [1, 2], is: true }
Generic with Interface
interface BoolCheck<T> {
value: T;
is: boolean;
}
function checkBoolValue<T>(arg: T): BoolCheck<T> {
if ([Link](arg) && ![Link]) {
return { value: arg, is: false };
}
if (isObj(arg) && .length) {
return { value: arg, is: false };
}
return { value: arg, is: !!arg };
}
Narrowing Generics with extends
interface HasID {
id: number;
}
function processUser<T extends HasID>(user: T): T {
// Process user
return user;
}
[Link](processUser({ id: 1, name: "Dave" })); // ✅ OK
[Link](processUser({ name: "Dave" })); // ❌ Error! No id
Multiple Generic Types
function getUsersProperty<T extends HasID, K extends keyof T>(
users: T[],
key: K
): T[K][] {
return [Link](user => user[key]);
}
const users = [
{
id: 1,
name: "Leanne Graham",
username: "Bret",
email: "Sincere@[Link]"
},
{
id: 2,
name: "Ervin Howell",
username: "Antonette",
email: "Shanna@[Link]"
}
];
[Link](getUsersProperty(users, "email"));
// ["Sincere@[Link]", "Shanna@[Link]"]
[Link](getUsersProperty(users, "username"));
// ["Bret", "Antonette"]
Breakdown:
T extends HasID - T must have an id property
K extends keyof T - K must be a key of T
users: T[] - Array of T objects
key: K - The key to extract
Returns: T[K][] - Array of values at that key
Generic Classes
class StateObject<T> {
private data: T;
constructor(value: T) {
[Link] = value;
}
get state(): T {
return [Link];
}
set state(value: T) {
[Link] = value;
}
}
// TypeScript infers type from initial value
const store = new StateObject("John");
[Link]([Link]); // "John"
[Link] = "Dave"; // ✅ OK
[Link] = 12; // ❌ Error! Must be string
// Explicitly specify type
const myState = new StateObject<(string | number | boolean)[]>([15]);
[Link] = ["Dave", 42, true]; // ✅ OK
[Link]([Link]);
Chapter 9: Utility Types
TypeScript provides many utility types for common type transformations.
Partial
Makes all properties optional:
interface Assignment {
studentId: string;
title: string;
grade: number;
verified?: boolean;
}
function updateAssignment(
assign: Assignment,
propsToUpdate: Partial<Assignment>
): Assignment {
return { ...assign, ...propsToUpdate };
}
const assign1: Assignment = {
studentId: "compsci123",
title: "Final Project",
grade: 0
};
const assignGraded = updateAssignment(assign1, { grade: 95 });
Required
Makes all properties required (opposite of Partial):
function recordAssignment(assign: Required<Assignment>): Assignment {
// Send to database, etc.
return assign;
}
// Must include 'verified' even though it's optional in interface
recordAssignment({ ...assignGraded, verified: true });
Readonly
Makes all properties readonly:
const assignVerified: Readonly<Assignment> = {
...assignGraded,
verified: true
};
[Link] = 88; // ❌ Error! Readonly property
Record<Keys, Type>
Creates an object type with specified keys and value types:
// Simple example
const hexColorMap: Record<string, string> = {
red: "FF0000",
green: "00FF00",
blue: "0000FF"
};
// With string literal types
type Students = "Sara" | "Kelly";
type LetterGrades = "A" | "B" | "C" | "D" | "U";
const finalGrades: Record<Students, LetterGrades> = {
Sara: "B",
Kelly: "U"
};
// With interface values
interface Grades {
assign1: number;
assign2: number;
}
const gradeData: Record<Students, Grades> = {
Sara: { assign1: 85, assign2: 93 },
Kelly: { assign1: 76, assign2: 15 }
};
Pick<Type, Keys>
Creates a type by picking specific properties:
type AssignResult = Pick<Assignment, "studentId" | "grade">;
const score: AssignResult = {
studentId: "k123",
grade: 85
};
Omit<Type, Keys>
Creates a type by omitting specific properties:
type AssignPreview = Omit<Assignment, "grade" | "verified">;
const preview: AssignPreview = {
studentId: "k123",
title: "Final Project"
};
Exclude<UnionType, ExcludedMembers>
Excludes types from a union (works with string literal types, not interfaces):
type LetterGrades = "A" | "B" | "C" | "D" | "U";
type AdjustedGrade = Exclude<LetterGrades, "U">; // "A" | "B" | "C" | "D"
Extract<UnionType, ExtractedMembers>
Extracts types from a union:
type HighGrades = Extract<LetterGrades, "A" | "B">; // "A" | "B"
NonNullable
Removes null and undefined from a type:
type AllPossibleGrades = "Dave" | "John" | null | undefined;
type NamesOnly = NonNullable<AllPossibleGrades>; // "Dave" | "John"
ReturnType
Extracts the return type of a function:
function createNewAssign(title: string, points: number) {
return { title, points };
}
type NewAssign = ReturnType<typeof createNewAssign>;
// { title: string; points: number; }
const tsAssign: NewAssign = createNewAssign("Utility Types", 100);
Benefits:
Type updates automatically when function changes
Useful for library functions you don't control
Parameters
Extracts parameter types as a tuple:
type AssignParams = Parameters<typeof createNewAssign>;
// [title: string, points: number]
const assignArgs: AssignParams = ["Generics", 100];
const tsAssign2: NewAssign = createNewAssign(...assignArgs);
Awaited
Unwraps the type of a Promise:
interface User {
id: number;
name: string;
username: string;
email: string;
}
function fetchUsers(): Promise<User[]> {
return fetch("[Link]
.then(res => [Link]())
.then(data => data)
.catch(error => {
if (error instanceof Error) [Link](error);
});
}
// Without Awaited - gets Promise<User[]>
type FetchUsersReturnType = ReturnType<typeof fetchUsers>;
// Promise<User[]>
// With Awaited - gets User[]
type FetchUsersReturn = Awaited<ReturnType<typeof fetchUsers>>;
// User[]
fetchUsers().then(users => [Link](users));
Quick Reference
Type Aliases vs Interfaces
Type Alias Interface
Can represent ANY type Best for objects/classes
Use for unions, primitives Cannot do unions
Use = syntax No = needed
Can't be merged Can be merged
Visibility Modifiers
Modifier Access
public (default) Everywhere
private Only within class
protected Within class and subclasses
readonly Can't be modified after initialization
Common Patterns
// Generic function
function identity<T>(arg: T): T {
return arg;
}
// Generic with constraint
function getProperty<T extends object, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
// Generic class
class Box<T> {
constructor(public contents: T) {}
}
// Generic interface
interface Container<T> {
value: T;
}
Key Takeaways
1. Type Aliases make code more readable and DRY
2. Literal types restrict values to specific options
3. Type assertions tell TypeScript you know better (use carefully!)
4. Classes support full OOP with visibility modifiers
5. Index signatures enable dynamic property access
6. Generics provide type safety with flexibility
7. Utility types offer powerful type transformations
Next: Continue with more advanced TypeScript topics!
TypeScript Tutorial Notes - Part 3
Course: TypeScript for Beginners (Chapters 10-14)
Instructor: Dave Gray
Chapter 10: [Link] + TypeScript
What is Vite?
Pronunciation: "Veet" (not "Vite")
Vite is a fast build tool that helps you start JavaScript, TypeScript, or React projects quickly. It's faster
than Create React App and works with multiple frameworks.
Prerequisites
[Link] - Download LTS version from [Link]
Check version: node -v
Creating a Vite Project
# Create new Vite project
npm create vite@latest
# Follow prompts:
# - Project name: vite-ts-project
# - Framework: vanilla (or react, preact, lit, etc.)
# - Variant: TypeScript
# Navigate to project
cd vite-ts-project
# Install dependencies
npm install
# or
npm i
# Run development server
npm run Dev
# Opens at [Link]
Project Structure
vite-ts-project/
├── src/
│ ├── [Link] # Entry point
│ ├── [Link] # Example component
│ └── [Link] # Environment variable types
├── [Link] # HTML entry
├── [Link] # TypeScript config
├── [Link] # Dependencies & scripts
└── [Link] # Vite configuration
TypeScript Configuration
Vite provides good defaults in [Link] :
Already includes "include": ["src"]
Configured for modern TypeScript usage
Ready to go without modifications
Environment Variables
Location: [Link]
// Access environment variables
[Link]
[Link]
[Link]
// Custom variables
[Link].VITE_API_KEY // Must start with VITE_
Note: Unlike other tools, Vite uses [Link] instead of [Link]
Creating a React + TypeScript Project
npm create vite@latest
# Choose:
# - Project name: vite-react-ts-project
# - Framework: react
# - Variant: TypeScript
cd vite-react-ts-project
npm i
npm run dev
React project includes:
[Link] (instead of .ts)
[Link] component
Vite React plugin pre-configured
TypeScript intellisense ready
[Link] Scripts
{
"scripts": {
"dev": "vite", // Start dev server
"build": "vite build", // Production build
"preview": "vite preview" // Preview production build
}
}
Deployment
Vite has deployment guides for:
Vercel
GitHub Pages
Netlify
Render
Firebase
Check official docs: [Link]/guide/static-deploy
Environment Files
.env # All environments
.[Link] # Local only (ignored by git)
.[Link] # Development only
.[Link] # Production only
Important: .[Link] is automatically ignored by git (already in .gitignore )
Why Vite?
Fast: Much faster than Create React App
Modern: Uses ES modules for development
Flexible: Works with multiple frameworks
TypeScript ready: Built-in TypeScript support
Hot Module Replacement (HMR): Instant updates during development
Chapter 11: TypeScript Project - Simple List App
Project Overview
Build a list application with:
Add items to list
Check items off
Delete individual items
Clear entire list
Persist data in localStorage
Project Setup
npm create vite@latest
# Name: lesson-11
# Framework: vanilla
# Variant: TypeScript
cd lesson-11
npm i
npm run dev
Project Structure
lesson-11/
├── src/
│ ├── css/
│ │ └── [Link]
│ ├── model/
│ │ ├── [Link]
│ │ └── [Link]
│ ├── templates/
│ │ └── [Link]
│ └── [Link]
├── build/
│ └── [Link]
└── [Link]
Model: [Link]
export interface Item {
id: string;
item: string;
checked: boolean;
}
export default class ListItem implements Item {
constructor(
private _id: string = "",
private _item: string = "",
private _checked: boolean = false
) {}
get id(): string {
return this._id;
}
set id(id: string) {
this._id = id;
}
get item(): string {
return this._item;
}
set item(item: string) {
this._item = item;
}
get checked(): boolean {
return this._checked;
}
set checked(checked: boolean) {
this._checked = checked;
}
}
Key concepts:
Private properties with underscore prefix
Getters and setters without underscore
Visibility modifiers in constructor params (shorthand)
Model: [Link] (Singleton)
import ListItem from "./ListItem";
interface List {
list: ListItem[];
load(): void;
save(): void;
clearList(): void;
addItem(itemObj: ListItem): void;
removeItem(id: string): void;
}
export default class FullList implements List {
static instance: FullList = new FullList();
private constructor(private _list: ListItem[] = []) {}
get list(): ListItem[] {
return this._list;
}
load(): void {
const storedList: string | null = [Link]("myList");
if (typeof storedList !== "string") return;
const parsedList: { _id: string; _item: string; _checked: boolean }[] =
[Link](storedList);
[Link]((itemObj) => {
const newListItem = new ListItem(
itemObj._id,
itemObj._item,
itemObj._checked
);
[Link](newListItem);
});
}
save(): void {
[Link]("myList", [Link](this._list));
}
clearList(): void {
this._list = [];
[Link]();
}
addItem(itemObj: ListItem): void {
this._list.push(itemObj);
[Link]();
}
removeItem(id: string): void {
this._list = this._list.filter((item) => [Link] !== id);
[Link]();
}
}
Singleton pattern:
Private constructor
Static instance property
Only one instance throughout app
Template: [Link]
import FullList from "../model/FullList";
interface DOMList {
ul: HTMLUListElement;
clear(): void;
render(fullList: FullList): void;
}
export default class ListTemplate implements DOMList {
static instance: ListTemplate = new ListTemplate();
ul: HTMLUListElement;
private constructor() {
[Link] = [Link]("listItems") as HTMLUListElement;
}
clear(): void {
[Link] = "";
}
render(fullList: FullList): void {
[Link]();
[Link]((item) => {
const li = [Link]("li") as HTMLLIElement;
[Link] = "item";
const check = [Link]("input") as HTMLInputElement;
[Link] = "checkbox";
[Link] = [Link];
[Link] = [Link];
[Link]("change", () => {
[Link] = ![Link];
[Link]();
});
[Link](check);
const label = [Link]("label") as HTMLLabelElement;
[Link] = [Link];
[Link] = [Link];
[Link](label);
const button = [Link]("button") as HTMLButtonElement;
[Link] = "button";
[Link] = "X";
[Link]("click", () => {
[Link]([Link]);
[Link](fullList);
});
[Link](button);
[Link](li);
});
}
}
Main Application: [Link]
import "./css/[Link]";
import FullList from "./model/FullList";
import ListItem from "./model/ListItem";
import ListTemplate from "./templates/ListTemplate";
const initApp = (): void => {
const fullList = [Link];
const template = [Link];
// Form submit handler
const itemEntryForm = [Link](
"itemEntryForm"
) as HTMLFormElement;
[Link]("submit", (event: SubmitEvent): void => {
[Link]();
const input = [Link]("newItem") as HTMLInputElement;
const newEntryText: string = [Link]();
if (![Link]) return;
const itemId: number = [Link]
? parseInt([Link][[Link] - 1].id) + 1
: 1;
const newItem = new ListItem([Link](), newEntryText);
[Link](newItem);
[Link](fullList);
[Link] = "";
});
// Clear button handler
const clearItems = [Link](
"clearItemsButton"
) as HTMLButtonElement;
[Link]("click", (): void => {
[Link]();
[Link]();
});
// Load and render on startup
[Link]();
[Link](fullList);
};
[Link]("DOMContentLoaded", initApp);
Key Patterns
Type Assertions:
const element = [Link]("id") as HTMLInputElement;
Singleton Access:
const instance = [Link];
LocalStorage:
// Save
[Link]("key", [Link](data));
// Load
const data = [Link]("key");
const parsed = [Link](data);
Chapter 12: React + TypeScript Basics
Project Setup
npm create vite@latest
# Name: lesson-12
# Framework: react
# Variant: TypeScript
cd lesson-12
npm i
npm run dev
File Extensions
.tsx - TypeScript + JSX (for components)
.ts - TypeScript only
Basic Function Component
import { ReactElement } from "react";
type HeadingProps = {
title: string;
};
const Heading = ({ title }: HeadingProps): ReactElement => {
return <h1>{title}</h1>;
};
export default Heading;
Return types:
ReactElement - Specific element
[Link] - Inferred (most common)
Components with Children
Modern approach (React 18+):
import { ReactNode } from "react";
type SectionProps = {
title?: string;
children: ReactNode; // Must be explicit in React 18+
};
const Section = ({
children,
title = "My Subheading"
}: SectionProps) => {
return (
<section>
<h2>{title}</h2>
{children}
</section>
);
};
export default Section;
What changed in React 18:
Children must be explicitly typed
Cannot implicitly include children anymore
ReactNode is the recommended type for children
Deprecated patterns:
// ❌ Don't use (deprecated)
const Component: [Link]<Props> = ({ children }) => { };
// ❌ Don't use (being deprecated)
[Link] = { title: "Default" };
// ✅ Use default parameters instead
const Component = ({ title = "Default" }: Props) => { };
useState Hook
import { useState } from "react";
const Counter = () => {
// Type inference works
const [count, setCount] = useState(0); // inferred as number
// Explicit typing
const [count, setCount] = useState<number>(0);
// Union types
const [data, setData] = useState<number | null>(null);
// With interfaces
interface User {
id: number;
name: string;
}
const [user, setUser] = useState<User | null>(null);
const [users, setUsers] = useState<User[]>([]);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(prev => prev + 1)}>+</button>
<button onClick={() => setCount(prev => prev - 1)}>-</button>
</div>
);
};
Passing State and Functions
type CounterProps = {
setCount: [Link]<[Link]<number>>;
children: ReactNode;
};
const Counter = ({ setCount, children }: CounterProps) => {
return (
<>
<h1>{children}</h1>
<button onClick={() => setCount(prev => prev + 1)}>+</button>
<button onClick={() => setCount(prev => prev - 1)}>-</button>
</>
);
};
// Usage in parent
const App = () => {
const [count, setCount] = useState(1);
return (
<Counter setCount={setCount}>
Count is: {count}
</Counter>
);
};
Generic List Component
import { ReactNode } from "react";
interface ListProps<T> {
items: T[];
render: (item: T) => ReactNode;
}
// Add comma after T to help TypeScript recognize generic
const List = <T,>({ items, render }: ListProps<T>) => {
return (
<ul>
{[Link]((item, i) => (
<li key={i}>{render(item)}</li>
))}
</ul>
);
};
// Usage
const App = () => {
const items = ["coffee", "tacos", "code"];
return (
<List
items={items}
render={(item: string) => <span className="bold">{item}</span>}
/>
);
};
Generic syntax note:
// Need comma after T with arrow functions
const Component = <T,>({ props }: Props<T>) => { };
// Or use extends
const Component = <T extends {}>({ props }: Props<T>) => { };
Chapter 13: React Hooks + TypeScript
useState Hook (Review)
import { useState } from "react";
// Inference (recommended for primitives)
const [count, setCount] = useState(0); // number
const [name, setName] = useState(""); // string
const [isActive, setActive] = useState(false); // boolean
// Explicit typing (for complex types)
interface User {
id: number;
username: string;
}
const [users, setUsers] = useState<User[]>([]);
const [user, setUser] = useState<User | null>(null);
// Type assertion (not recommended)
const [user, setUser] = useState({} as User); // Lying to compiler!
useEffect Hook
import { useEffect } from "react";
useEffect(() => {
[Link]("mounting");
// Cleanup function
return () => {
[Link]("unmounting");
};
}, []); // Empty array - runs once on mount
// With dependencies
useEffect(() => {
[Link]("users:", users);
}, [users]); // Runs when users changes
Note: No TypeScript-specific typing needed - useEffect doesn't return values.
useCallback Hook
import { useCallback, MouseEvent, KeyboardEvent } from "react";
const App = () => {
const [count, setCount] = useState(0);
// Basic useCallback
const addTwo = useCallback((): void => {
setCount(prev => prev + 2);
}, []); // No dependencies
// With event typing
const handleClick = useCallback(
(e: MouseEvent<HTMLButtonElement> | KeyboardEvent<HTMLButtonElement>): void =>
{
setCount(prev => prev + 1);
},
[]
);
return <button onClick={addTwo}>Add 2</button>;
};
Event types:
MouseEvent<HTMLButtonElement>
KeyboardEvent<HTMLButtonElement>
ChangeEvent<HTMLInputElement>
FormEvent<HTMLFormElement>
FocusEvent<HTMLInputElement>
useMemo Hook
import { useMemo } from "react";
type FibFunc = (n: number) => number;
const fib: FibFunc = (n) => {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
};
const App = () => {
const myNum = 37;
// Memoized value
const result = useMemo<number>(() => fib(myNum), [myNum]);
return <h2>{result}</h2>;
};
When to use:
Expensive calculations
Complex object creation
Avoid re-computation on every render
useRef Hook
import { useRef } from "react";
const App = () => {
// Type the element explicitly
const inputRef = useRef<HTMLInputElement>(null);
// Non-null assertion alternative
const inputRef = useRef<HTMLInputElement>(null!);
// Access with optional chaining (safer)
[Link]([Link]); // Element or null
[Link]([Link]?.value); // Value or undefined
return <input type="text" ref={inputRef} />;
};
Important:
Changing [Link] doesn't trigger re-render
Use optional chaining when accessing values
Specify element type: HTMLInputElement , HTMLDivElement , etc.
Chapter 14: React useReducer + TypeScript
Basic Counter with useState (Before)
import { useState } from "react";
const Counter = () => {
const [count, setCount] = useState<number>(1);
const increment = () => setCount(prev => prev + 1);
const decrement = () => setCount(prev => prev - 1);
return (
<>
<h1>Count is: {count}</h1>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</>
);
};
Refactored with useReducer
1. Define Initial State
const initState = { count: 0 };
TypeScript infers: { count: number }
2. Define Action Types
// Using enum (controversial but valid)
const enum ReducerActionType {
INCREMENT,
DECREMENT,
NEW_INPUT
}
// Alternative: String literal union
type ReducerActionType = "INCREMENT" | "DECREMENT" | "NEW_INPUT";
3. Define Action Type
type ReducerAction = {
type: ReducerActionType;
payload?: string; // Optional for actions that don't need it
};
4. Create Reducer Function
const reducer = (
state: typeof initState,
action: ReducerAction
): typeof initState => {
switch ([Link]) {
case [Link]:
return { ...state, count: [Link] + 1 };
case [Link]:
return { ...state, count: [Link] - 1 };
case ReducerActionType.NEW_INPUT:
return { ...state, text: [Link] ?? "" };
default:
throw new Error();
}
};
Using typeof for state type:
Infers type from initial state
Keeps types in sync automatically
5. Use in Component
import { useReducer, ChangeEvent } from "react";
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initState);
const increment = () =>
dispatch({ type: [Link] });
const decrement = () =>
dispatch({ type: [Link] });
const handleTextInput = (e: ChangeEvent<HTMLInputElement>) => {
dispatch({
type: ReducerActionType.NEW_INPUT,
payload: [Link]
});
};
return (
<>
<h1>Count is: {[Link]}</h1>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<input type="text" onChange={handleTextInput} />
<h2>{[Link]}</h2>
</>
);
};
Complete Example with Text Input
import { useReducer, ChangeEvent } from "react";
// Initial state
const initState = {
count: 0,
text: ""
};
// Action types
const enum ReducerActionType {
INCREMENT,
DECREMENT,
NEW_INPUT
}
// Action type definition
type ReducerAction = {
type: ReducerActionType;
payload?: string;
};
// Reducer function
const reducer = (
state: typeof initState,
action: ReducerAction
): typeof initState => {
switch ([Link]) {
case [Link]:
return { ...state, count: [Link] + 1 };
case [Link]:
return { ...state, count: [Link] - 1 };
case ReducerActionType.NEW_INPUT:
return { ...state, text: [Link] ?? "" };
default:
throw new Error();
}
};
// Component
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initState);
const increment = () =>
dispatch({ type: [Link] });
const decrement = () =>
dispatch({ type: [Link] });
const handleTextInput = (e: ChangeEvent<HTMLInputElement>) => {
dispatch({
type: ReducerActionType.NEW_INPUT,
payload: [Link]
});
};
return (
<>
<h1>Count is: {[Link]}</h1>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<input type="text" onChange={handleTextInput} />
<h2>{[Link]}</h2>
</>
);
};
export default Counter;
Handling Optional Payloads
Problem:
text: [Link] // Error: Type 'string | undefined' not assignable
Solution - Nullish Coalescing:
text: [Link] ?? "" // Use empty string if undefined
When to Use useReducer
Use useReducer when:
Complex state logic with multiple sub-values
State updates depend on previous state
Need to share state logic between components (with Context)
Many related state updates
Stick with useState when:
Simple state (single value)
Independent state updates
No complex state transitions
Quick Reference
React Component Patterns
// Basic component
type Props = { title: string };
const Component = ({ title }: Props) => <h1>{title}</h1>;
// With children
type Props = { children: ReactNode };
const Component = ({ children }: Props) => <div>{children}</div>;
// With optional props
type Props = { title?: string };
const Component = ({ title = "Default" }: Props) => <h1>{title}</h1>;
Common Event Types
MouseEvent<HTMLButtonElement>
ChangeEvent<HTMLInputElement>
FormEvent<HTMLFormElement>
KeyboardEvent<HTMLInputElement>
FocusEvent<HTMLInputElement>
Hook Patterns
// useState
const [state, setState] = useState<Type>(initialValue);
// useReducer
const [state, dispatch] = useReducer(reducer, initialState);
// useRef
const ref = useRef<HTMLInputElement>(null);
// useCallback
const fn = useCallback(() => { }, [deps]);
// useMemo
const value = useMemo(() => compute(), [deps]);
Key Takeaways
1. Vite is faster and simpler than Create React App
2. React 18 requires explicit ReactNode type for children
3. Deprecated: [Link] , defaultProps
4. useState often infers types automatically
5. useReducer is great for complex state with multiple sub-values
6. Event types must be specified in React 18+
7. Generic components need comma or extends for TypeScript recognition
8. useRef requires element type specification
9. Singleton pattern useful for global state management
Next: useContext and custom hooks with TypeScript!
TypeScript Tutorial Notes - Part 4 (Complete Final)
Course: TypeScript for Beginners (Chapters 15-17 Complete)
Instructor: Dave Gray
Chapter 15: React useContext + TypeScript (Complete)
Starting Point
Begin with code from Lesson 14 (useReducer). We'll extract all state and logic from the Counter
component into a Context.
Why Use Context?
Problems it solves:
Unclutters components
Organizes state and logic by feature
Avoids prop drilling
Easier to maintain as app grows
Complete Context Implementation
1. Define State and Action Types
// State type
type StateType = {
count: number;
text: string;
};
const initState: StateType = { count: 0, text: "" };
// Action types
const enum ReducerActionType {
INCREMENT,
DECREMENT,
NEW_INPUT
}
type ReducerAction = {
type: ReducerActionType;
payload?: string;
};
2. Create Reducer Function
const reducer = (
state: StateType,
action: ReducerAction
): StateType => {
switch ([Link]) {
case [Link]:
return { ...state, count: [Link] + 1 };
case [Link]:
return { ...state, count: [Link] - 1 };
case ReducerActionType.NEW_INPUT:
return { ...state, text: [Link] ?? "" };
default:
throw new Error();
}
};
3. Create Internal Custom Hook (NOT Exported)
import { useReducer, useCallback, ChangeEvent } from "react";
const useCounterContext = (initState: StateType) => {
const [state, dispatch] = useReducer(reducer, initState);
// Wrap in useCallback for referential equality
const increment = useCallback(
() => dispatch({ type: [Link] }),
[]
);
const decrement = useCallback(
() => dispatch({ type: [Link] }),
[]
);
const handleTextInput = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch({
type: ReducerActionType.NEW_INPUT,
payload: [Link]
});
},
[]
);
return { state, increment, decrement, handleTextInput };
};
Critical: Wrap functions in useCallback to prevent unnecessary re-renders!
4. Create Context Type
// Use ReturnType utility to extract type from hook
type UseCounterContextType = ReturnType<typeof useCounterContext>;
// Initial context state (dummy functions for initialization)
const initContextState: UseCounterContextType = {
state: initState,
increment: () => {},
decrement: () => {},
handleTextInput: (e: ChangeEvent<HTMLInputElement>) => {}
};
Why dummy functions? Context needs initial values, but they'll be replaced by provider.
5. Create Context
import { createContext, ReactElement } from "react";
export const CounterContext = createContext<UseCounterContextType>(
initContextState
);
6. Create Children Type (React 18+)
type ChildrenType = {
children?: ReactElement | ReactElement[];
};
Important: Children must be explicitly typed in React 18+
7. Create Provider
export const CounterProvider = ({
children,
...initState
}: ChildrenType & StateType): ReactElement => {
return (
<[Link] value={useCounterContext(initState)}>
{children}
</[Link]>
);
};
export default CounterContext;
8. Wrap App with Provider
// [Link]
import { CounterProvider } from './context/CounterContext';
import { initState } from './context/CounterContext';
<CounterProvider count={[Link]} text={[Link]}>
<Counter />
</CounterProvider>
9. Create Custom Hook for Consumers
// hooks/[Link]
import { useContext } from "react";
import CounterContext from "../context/CounterContext";
import { UseCounterContextType } from "../context/CounterContext";
const useCounter = (): UseCounterContextType => {
return useContext(CounterContext);
};
export default useCounter;
Why create this hook? Cleaner than importing both useContext and CounterContext .
10. Use in Components
import useCounter from "../hooks/useCounter";
const Counter = () => {
const { state, increment, decrement } = useCounter();
return (
<>
<h1>Count: {[Link]}</h1>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</>
);
};
Complete File Structure
src/
├── context/
│ └── [Link]
├── hooks/
│ └── [Link]
└── components/
└── [Link]
What to Export from Context
// [Link] exports:
export const CounterContext; // Context itself
export const CounterProvider; // Provider component
export const initState; // Initial state
export type UseCounterContextType; // Type (if needed elsewhere)
// DON'T export:
// - useCounterContext (internal hook)
Key Patterns Summary
1. Internal hook - Creates state/logic, NOT exported
2. ReturnType utility - Gets type from hook automatically
3. useCallback - Prevents function recreation
4. Custom consumer hook - Cleaner component imports
5. Children typing - Required in React 18+
Chapters 16-17: Shopping Cart Project (Complete)
Project Overview
Features:
Display products from data
Add products to cart
Update quantities in cart
Remove items from cart
Display totals (items & price)
Toggle product/cart views
Format currency properly
Memoize components for performance
Project Setup
npm create vite@latest
# Name: lesson-16
# Framework: react
# Variant: TypeScript
cd lesson-16
npm i
npm run dev
Complete File Structure
src/
├── components/
│ ├── [Link]
│ ├── [Link]
│ ├── [Link]
│ ├── [Link]
│ ├── [Link]
│ ├── [Link]
│ └── [Link]
├── context/
│ ├── [Link]
│ └── [Link]
├── hooks/
│ ├── [Link]
│ └── [Link]
├── images/
│ ├── [Link]
│ ├── [Link]
│ └── [Link]
├── [Link]
├── [Link]
└── [Link]
data/
└── [Link]
Part 1: Context Setup
Products Data
// data/[Link]
{
"products": [
{
"sku": "item0001",
"name": "Widget",
"price": 9.99
},
{
"sku": "item0002",
"name": "Premium Widget",
"price": 19.99
},
{
"sku": "item0003",
"name": "Deluxe Widget",
"price": 29.99
}
]
}
[Link] (Complete)
import { createContext, ReactElement, useState } from "react";
// Product type
export type ProductType = {
sku: string;
name: string;
price: number;
};
// Initial state (hardcoded for deployment)
const initState: ProductType[] = [
{ "sku": "item0001", "name": "Widget", "price": 9.99 },
{ "sku": "item0002", "name": "Premium Widget", "price": 19.99 },
{ "sku": "item0003", "name": "Deluxe Widget", "price": 29.99 }
];
// Alternative: Fetch from JSON server (dev only)
/*
import { useEffect } from "react";
const initState: ProductType[] = [];
// In provider component:
useEffect(() => {
const fetchProducts = async (): Promise<ProductType[]> => {
const data = await fetch("[Link]
.then(res => [Link]())
.catch(err => {
if (err instanceof Error) [Link]([Link]);
});
return data;
};
fetchProducts().then(products => setProducts(products));
}, []);
*/
// Context type
export type UseProductsContextType = {
products: ProductType[];
};
// Initial context state
const initContextState: UseProductsContextType = {
products: []
};
// Create context
const ProductsContext = createContext<UseProductsContextType>(
initContextState
);
// Children type
type ChildrenType = {
children?: ReactElement | ReactElement[];
};
// Provider
export const ProductsProvider = ({
children
}: ChildrenType): ReactElement => {
const [products] = useState<ProductType[]>(initState);
return (
<[Link] value={{ products }}>
{children}
</[Link]>
);
};
export default ProductsContext;
Deployment note: Use hardcoded initState for production. JSON server only for development.
[Link] (Complete)
import { useReducer, useMemo, createContext, ReactElement } from "react";
// Types
export type CartItemType = {
sku: string;
name: string;
price: number;
qty: number;
};
type CartStateType = {
cart: CartItemType[];
};
const initCartState: CartStateType = { cart: [] };
// Action types (using object instead of enum)
const REDUCER_ACTION_TYPE = {
ADD: "ADD",
REMOVE: "REMOVE",
QUANTITY: "QUANTITY",
SUBMIT: "SUBMIT"
};
export type ReducerActionType = typeof REDUCER_ACTION_TYPE;
export type ReducerAction = {
type: string;
payload?: CartItemType;
};
// Reducer function
const reducer = (
state: CartStateType,
action: ReducerAction
): CartStateType => {
switch ([Link]) {
case REDUCER_ACTION_TYPE.ADD: {
if (![Link]) {
throw new Error("[Link] missing in ADD action");
}
const { sku, name, price } = [Link];
const filteredCart: CartItemType[] = [Link](
item => [Link] !== sku
);
const itemExists: CartItemType | undefined = [Link](
item => [Link] === sku
);
const qty: number = itemExists ? [Link] + 1 : 1;
return {
...state,
cart: [...filteredCart, { sku, name, price, qty }]
};
}
case REDUCER_ACTION_TYPE.REMOVE: {
if (![Link]) {
throw new Error("[Link] missing in REMOVE action");
}
const { sku } = [Link];
const filteredCart: CartItemType[] = [Link](
item => [Link] !== sku
);
return { ...state, cart: [...filteredCart] };
}
case REDUCER_ACTION_TYPE.QUANTITY: {
if (![Link]) {
throw new Error("[Link] missing in QUANTITY action");
}
const { sku, qty } = [Link];
const itemExists: CartItemType | undefined = [Link](
item => [Link] === sku
);
if (!itemExists) {
throw new Error("Item must exist to update quantity");
}
const updatedItem: CartItemType = { ...itemExists, qty };
const filteredCart: CartItemType[] = [Link](
item => [Link] !== sku
);
return {
...state,
cart: [...filteredCart, updatedItem]
};
}
case REDUCER_ACTION_TYPE.SUBMIT: {
return { ...state, cart: [] };
}
default:
throw new Error("Unidentified reducer action type");
}
};
// Internal hook (NOT exported)
const useCartContext = (initState: CartStateType) => {
const [state, dispatch] = useReducer(reducer, initState);
const REDUCER_ACTIONS = useMemo(() => REDUCER_ACTION_TYPE, []);
const totalItems: number = [Link]((prev, item) => {
return prev + [Link];
}, 0);
const totalPrice: string = new [Link]("en-US", {
style: "currency",
currency: "USD"
}).format(
[Link]((prev, item) => {
return prev + [Link] * [Link];
}, 0)
);
const cart = [Link]((a, b) => {
const itemA = Number([Link](-4));
const itemB = Number([Link](-4));
return itemA - itemB;
});
return { dispatch, REDUCER_ACTIONS, totalItems, totalPrice, cart };
};
// Context type
export type UseCartContextType = ReturnType<typeof useCartContext>;
// Initial context state
const initCartContextState: UseCartContextType = {
dispatch: () => {},
REDUCER_ACTIONS: REDUCER_ACTION_TYPE,
totalItems: 0,
totalPrice: "",
cart: []
};
// Create context
const CartContext = createContext<UseCartContextType>(initCartContextState);
// Children type
type ChildrenType = {
children?: ReactElement | ReactElement[];
};
// Provider
export const CartProvider = ({ children }: ChildrenType): ReactElement => {
return (
<[Link] value={useCartContext(initCartState)}>
{children}
</[Link]>
);
};
export default CartContext;
Key calculations:
totalItems - Sum all quantities
totalPrice - Formatted currency string
cart - Sorted by SKU number
REDUCER_ACTIONS - Memoized for performance
Part 2: Custom Hooks
[Link]
import { useContext } from "react";
import CartContext from "../context/CartProvider";
import { UseCartContextType } from "../context/CartProvider";
const useCart = (): UseCartContextType => {
return useContext(CartContext);
};
export default useCart;
[Link]
import { useContext } from "react";
import ProductsContext from "../context/ProductsProvider";
import { UseProductsContextType } from "../context/ProductsProvider";
const useProducts = (): UseProductsContextType => {
return useContext(ProductsContext);
};
export default useProducts;
Why create these? Much cleaner than importing both useContext and the Context in every
component.
Part 3: App Structure
[Link] - Wrap with Providers
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./[Link]";
import "./[Link]";
import { CartProvider } from "./context/CartProvider";
import { ProductsProvider } from "./context/ProductsProvider";
createRoot([Link]("root")!).render(
<StrictMode>
<ProductsProvider>
<CartProvider>
<App />
</CartProvider>
</ProductsProvider>
</StrictMode>
);
Order matters: Products wraps Cart (Cart depends on Products).
[Link] (Complete)
import { useState } from "react";
import Header from "./components/Header";
import Footer from "./components/Footer";
import Cart from "./components/Cart";
import ProductList from "./components/ProductList";
function App() {
const [viewCart, setViewCart] = useState<boolean>(false);
const pageContent = viewCart ? <Cart /> : <ProductList />;
const content = (
<>
<Header viewCart={viewCart} setViewCart={setViewCart} />
{pageContent}
<Footer viewCart={viewCart} />
</>
);
return content;
}
export default App;
Part 4: Header & Navigation
[Link]
import Nav from "./Nav";
import useCart from "../hooks/useCart";
type PropsType = {
viewCart: boolean;
setViewCart: [Link]<[Link]<boolean>>;
};
const Header = ({ viewCart, setViewCart }: PropsType) => {
const { totalItems, totalPrice } = useCart();
const content = (
<header className="header">
<div className="header__title-bar">
<h1>Acme Co.</h1>
<div className="header__price-box">
<p>Total Items: {totalItems}</p>
<p>Total Price: {totalPrice}</p>
</div>
</div>
<Nav viewCart={viewCart} setViewCart={setViewCart} />
</header>
);
return content;
};
export default Header;
[Link]
type PropsType = {
viewCart: boolean;
setViewCart: [Link]<[Link]<boolean>>;
};
const Nav = ({ viewCart, setViewCart }: PropsType) => {
const button = viewCart
? <button onClick={() => setViewCart(false)}>View Products</button>
: <button onClick={() => setViewCart(true)}>View Cart</button>;
const content = <nav className="nav">{button}</nav>;
return content;
};
export default Nav;
[Link]
import useCart from "../hooks/useCart";
type PropsType = {
viewCart: boolean;
};
const Footer = ({ viewCart }: PropsType) => {
const { totalItems, totalPrice } = useCart();
const year: number = new Date().getFullYear();
const pageContent = viewCart ? (
<p>Shopping Cart © {year}</p>
) : (
<>
<p>Total Items: {totalItems}</p>
<p>Total Price: {totalPrice}</p>
<p>Shopping Cart © {year}</p>
</>
);
const content = <footer className="footer">{pageContent}</footer>;
return content;
};
export default Footer;
Part 5: Product Display
Critical: Dynamic Images with Vite
Problem: require() doesn't work with Vite
Old way (doesn't work with Vite):
// ❌ DON'T DO THIS
const img: string = require(`../images/${[Link]}.jpg`);
New way (works with Vite AND Create React App):
// ✅ DO THIS
const img: string = new URL(
`../images/${[Link]}.jpg`,
[Link]
).href;
How it works:
Creates URL object from relative path
[Link] provides base URL
.href extracts the string path
Vite recognizes this pattern and updates URLs in build
Development vs Production:
Dev: Points to local file path
Production: Points to bundled asset path
Vite automatically handles the conversion!
[Link]
import { ReactElement } from "react";
import useCart from "../hooks/useCart";
import useProducts from "../hooks/useProducts";
import Product from "./Product";
const ProductList = () => {
const { dispatch, REDUCER_ACTIONS, cart } = useCart();
const { products } = useProducts();
let pageContent: ReactElement | ReactElement[] = <p>Loading...</p>;
if (products?.length) {
pageContent = [Link](product => {
const inCart: boolean = [Link](item => [Link] === [Link]);
return (
<Product
key={[Link]}
product={product}
dispatch={dispatch}
REDUCER_ACTIONS={REDUCER_ACTIONS}
inCart={inCart}
/>
);
});
}
const content = (
<main className="main main--products">
{pageContent}
</main>
);
return content;
};
export default ProductList;
[Link] (Without Memoization)
import { ReactElement } from "react";
import { ProductType } from "../context/ProductsProvider";
import { ReducerActionType, ReducerAction } from "../context/CartProvider";
type PropsType = {
product: ProductType;
dispatch: [Link]<ReducerAction>;
REDUCER_ACTIONS: ReducerActionType;
inCart: boolean;
};
const Product = ({
product,
dispatch,
REDUCER_ACTIONS,
inCart
}: PropsType): ReactElement => {
const img: string = new URL(
`../images/${[Link]}.jpg`,
[Link]
).href;
const onAddToCart = () =>
dispatch({
type: REDUCER_ACTIONS.ADD,
payload: { ...product, qty: 1 }
});
const itemInCart = inCart ? " → ✔️" : null;
const content = (
<article className="product">
<h3>{[Link]}</h3>
<img src={img} alt={[Link]} className="product__img" />
<p>
{new [Link]("en-US", {
style: "currency",
currency: "USD"
}).format([Link])}
{itemInCart}
</p>
<button onClick={onAddToCart}>Add to Cart</button>
</article>
);
return content;
};
export default Product;
Part 6: Cart Display
[Link]
import { useState } from "react";
import useCart from "../hooks/useCart";
import CartLineItem from "./CartLineItem";
const Cart = () => {
const [confirm, setConfirm] = useState<boolean>(false);
const { dispatch, REDUCER_ACTIONS, totalItems, totalPrice, cart } = useCart();
const onSubmitOrder = () => {
dispatch({ type: REDUCER_ACTIONS.SUBMIT });
setConfirm(true);
};
const pageContent = confirm ? (
<h2>Thank you for your order.</h2>
) : (
<>
<h2 className="offscreen">Cart</h2>
<ul className="cart">
{[Link](item => (
<CartLineItem
key={[Link]}
item={item}
dispatch={dispatch}
REDUCER_ACTIONS={REDUCER_ACTIONS}
/>
))}
</ul>
<div className="cart__totals">
<p>Total Items: {totalItems}</p>
<p>Total Price: {totalPrice}</p>
<button
className="cart__submit"
disabled={!totalItems}
onClick={onSubmitOrder}
>
Place Order
</button>
</div>
</>
);
const content = <main className="main main--cart">{pageContent}</main>;
return content;
};
export default Cart;
[Link] (Without Memoization)
import { ChangeEvent, ReactElement } from "react";
import { CartItemType } from "../context/CartProvider";
import { ReducerAction, ReducerActionType } from "../context/CartProvider";
type PropsType = {
item: CartItemType;
dispatch: [Link]<ReducerAction>;
REDUCER_ACTIONS: ReducerActionType;
};
const CartLineItem = ({
item,
dispatch,
REDUCER_ACTIONS
}: PropsType): ReactElement => {
const img: string = new URL(
`../images/${[Link]}.jpg`,
[Link]
).href;
const lineTotal: number = [Link] * [Link];
const highestQty: number = 20 > [Link] ? 20 : [Link];
const optionValues: number[] = [...Array(highestQty).keys()].map(i => i + 1);
const options: ReactElement[] = [Link](val => (
<option key={`opt${val}`} value={val}>
{val}
</option>
));
const onChangeQty = (e: ChangeEvent<HTMLSelectElement>) => {
dispatch({
type: REDUCER_ACTIONS.QUANTITY,
payload: { ...item, qty: Number([Link]) }
});
};
const onRemoveFromCart = () =>
dispatch({ type: REDUCER_ACTIONS.REMOVE, payload: item });
const content = (
<li className="cart__item">
<img src={img} alt={[Link]} className="cart__img" />
<div aria-label="Item Name">{[Link]}</div>
<div aria-label="Price Per Item">
{new [Link]("en-US", {
style: "currency",
currency: "USD"
}).format([Link])}
</div>
<label htmlFor="itemQty" className="offscreen">
Item Quantity
</label>
<select
name="itemQty"
id="itemQty"
className="cart__select"
value={[Link]}
aria-label="Item Quantity"
onChange={onChangeQty}
>
{options}
</select>
<div className="cart__item-subtotal" aria-label="Line Item Subtotal">
{new [Link]("en-US", {
style: "currency",
currency: "USD"
}).format(lineTotal)}
</div>
<button
className="cart__button"
aria-label="Remove Item From Cart"
title="Remove Item From Cart"
onClick={onRemoveFromCart}
>
❌
</button>
</li>
);
return content;
};
export default CartLineItem;
Quantity dropdown logic:
Default max: 20
If [Link] > 20, max becomes [Link]
Creates array: [1, 2, 3, ..., max]
Part 7: Performance Optimization with [Link]
Why Memoize?
Problem: Objects lack referential equality
Product/CartItem objects recreated on every render
Causes unnecessary re-renders of child components
Solution: Use [Link] with custom comparison function
Memoized Product Component
import { ReactElement, memo } from "react";
import { ProductType } from "../context/ProductsProvider";
import { ReducerActionType, ReducerAction } from "../context/CartProvider";
type PropsType = {
product: ProductType;
dispatch: [Link]<ReducerAction>;
REDUCER_ACTIONS: ReducerActionType;
inCart: boolean;
};
const Product = ({
product,
dispatch,
REDUCER_ACTIONS,
inCart
}: PropsType): ReactElement => {
// ... component code (same as before)
};
// Comparison function
function areProductsEqual(
{ product: prevProduct, inCart: prevInCart }: PropsType,
{ product: nextProduct, inCart: nextInCart }: PropsType
) {
return (
[Link](prevProduct).every(key => {
return (
prevProduct[key as keyof ProductType] ===
nextProduct[key as keyof ProductType]
);
}) && prevInCart === nextInCart
);
}
// Memoized component
const MemoizedProduct = memo<PropsType>(Product, areProductsEqual);
export default MemoizedProduct;
How comparison works:
1. [Link](prevProduct) - Get array of keys
2. .every() - Check all keys match
3. key as keyof ProductType - TypeScript assertion for dynamic access
4. Compare each property value
5. Also compare inCart boolean separately
Memoized CartLineItem Component
import { ChangeEvent, ReactElement, memo } from "react";
import { CartItemType } from "../context/CartProvider";
import { ReducerAction, ReducerActionType } from "../context/CartProvider";
type PropsType = {
item: CartItemType;
dispatch: [Link]<ReducerAction>;
REDUCER_ACTIONS: ReducerActionType;
};
const CartLineItem = ({
item,
dispatch,
REDUCER_ACTIONS
}: PropsType): ReactElement => {
// ... component code (same as before)
};
// Comparison function
function areItemsEqual(
{ item: prevItem }: PropsType,
{ item: nextItem }: PropsType
) {
return [Link](prevItem).every(key => {
return (
prevItem[key as keyof CartItemType] ===
nextItem[key as keyof CartItemType]
);
});
}
// Memoized component
const MemoizedCartLineItem = memo<PropsType>(CartLineItem, areItemsEqual);
export default MemoizedCartLineItem;
What gets memoized:
item object properties
NOT dispatch (already has referential equality)
NOT REDUCER_ACTIONS (memoized in context with useMemo)
Testing Memoization
Use React DevTools:
1. Enable "Highlight updates when components render"
2. Add item to cart - only that product should highlight
3. Change quantity - only that cart line item should highlight
4. Other components should NOT re-render
Complete Project Patterns
Currency Formatting
new [Link]("en-US", {
style: "currency",
currency: "USD"
}).format(price)
Returns: "$9.99" (formatted string with symbol)
Reducer Pattern for ADD
case REDUCER_ACTION_TYPE.ADD: {
const { sku, name, price } = [Link];
// Filter out item if exists
const filteredCart = [Link](item => [Link] !== sku);
// Check if item exists
const itemExists = [Link](item => [Link] === sku);
// Calculate new quantity
const qty = itemExists ? [Link] + 1 : 1;
// Return new state
return {
...state,
cart: [...filteredCart, { sku, name, price, qty }]
};
}
Logic:
1. Remove item from cart (if exists)
2. Check if item existed
3. Increment qty or set to 1
4. Add item back with new qty
Dynamic Quantity Options
// Max 20, or item qty if higher
const highestQty: number = 20 > [Link] ? 20 : [Link];
// Create array [1, 2, 3, ..., highestQty]
const optionValues: number[] = [...Array(highestQty).keys()].map(i => i + 1);
// Map to option elements
const options: ReactElement[] = [Link](val => (
<option key={`opt${val}`} value={val}>{val}</option>
));
Sort Cart by SKU
const cart = [Link]((a, b) => {
const itemA = Number([Link](-4)); // "item0001" → 1
const itemB = Number([Link](-4)); // "item0002" → 2
return itemA - itemB;
});
Extract last 4 digits: slice(-4) gets "0001" from "item0001"
Complete Testing Checklist
Products display correctly
Add to cart shows checkmark
Header totals update
Footer totals update
Toggle to cart view
Cart displays items
Quantity dropdown works
Update quantity updates totals
Remove item works
Place order clears cart
Back to products view
Memoization prevents unnecessary re-renders
Images display correctly
Currency formats properly
No console errors
Common Issues & Solutions
Issue: Images not loading in production
Solution: Use new URL() pattern, not require()
// ✅ Correct
const img = new URL(`../images/${sku}.jpg`, [Link]).href;
// ❌ Wrong
const img = require(`../images/${sku}.jpg`);
Issue: Components re-rendering unnecessarily
Solution:
1. Memoize objects in context with useMemo
2. Memoize functions with useCallback
3. Use [Link] with comparison function for list items
Issue: TypeScript error on dynamic object keys
Solution: Use keyof assertion
// ❌ Error
obj[key]
// ✅ Correct
obj[key as keyof ObjectType]
Issue: Optional payload causing type errors
Solution: Use nullish coalescing
// ❌ Error
text: [Link] // Could be undefined
// ✅ Correct
text: [Link] ?? ""
Key Takeaways
Context Best Practices
1. Separate contexts by feature - Products, Cart, Auth, etc.
2. Create custom hooks - Cleaner than importing useContext everywhere
3. Memoize in context - Use useCallback and useMemo
4. Export provider - Let consumers import provider, not internal hook
5. Type everything - Use ReturnType utility for hook types
Performance Best Practices
1. Memoize action objects - Prevents re-renders
2. Wrap functions in useCallback - Maintains referential equality
3. Use [Link] for list items - Prevents unnecessary updates
4. Don't over-memoize - Simple calculations don't need memoization
5. Test with React DevTools - Verify re-render behavior
TypeScript Patterns
// ReturnType utility
type HookType = ReturnType<typeof customHook>;
// keyof assertion for dynamic keys
obj[key as keyof ObjectType]
// Children type (React 18+)
type ChildrenType = {
children?: ReactElement | ReactElement[];
};
// Dispatch type
[Link]<[Link]<T>>
[Link]<ReducerAction>
// Event types
ChangeEvent<HTMLInputElement>
ChangeEvent<HTMLSelectElement>
Vite Patterns
// Dynamic images
new URL(`../images/${name}.jpg`, [Link]).href
// Environment variables
[Link].VITE_API_KEY
// JSON server (dev only)
npx json-server -w data/[Link] -p 3500
Final Project Structure
lesson-16/
├── src/
│ ├── components/
│ │ ├── [Link] # Display totals, nav
│ │ ├── [Link] # Toggle button
│ │ ├── [Link] # Display totals/copyright
│ │ ├── [Link] # Map products
│ │ ├── [Link] # Individual product (memoized)
│ │ ├── [Link] # Cart view
│ │ └── [Link] # Cart item (memoized)
│ ├── context/
│ │ ├── [Link] # Products state
│ │ └── [Link] # Cart state & logic
│ ├── hooks/
│ │ ├── [Link] # Cart context hook
│ │ └── [Link] # Products context hook
│ ├── images/
│ │ ├── [Link]
│ │ ├── [Link]
│ │ └── [Link]
│ ├── [Link] # Main component
│ ├── [Link] # Entry point with providers
│ └── [Link] # Styles
├── data/
│ └── [Link] # Product data (dev only)
└── [Link]
Congratulations! 🎉
You've completed a full-featured shopping cart with:
✅ React + TypeScript
✅ Context API for state management
✅ useReducer for complex state logic
✅ Custom hooks for clean code
✅ Performance optimization with [Link]
✅ Type-safe development throughout
✅ Dynamic image handling with Vite
✅ Currency formatting
✅ Proper accessibility attributes
Remember: Progress over perfection. Keep coding! 🚀
Complete TypeScript Tutorial Series - Finished!