Intersection types
Intersection types let you combine multiple types into one using &.
- For object types, an intersection generally means “must satisfy all properties from both sides.”
- For union types, an intersection means “the overlap between the unions” (sometimes that overlap is
neverif there’s no common member).
type Admin = {
name: string;
privileges: string[]
}
type Employee = {
name: string;
startDate: Date;
}
type ElevatedEmployee = Admin & Employee // equivalent of interface Elevatedemployee extends Employee, Admin {}
const e1: ElevatedEmployee = {
name: 'Max',
privileges: ['create-server'],
startDate: new Date()
}
type Combinable = string | number;
type Numeric = number | boolean
type Universal = combinable & Numeric // universal ends up being number.
Key takeaways:
ElevatedEmployeemust havename,privileges, andstartDate.(string | number) & (number | boolean)resolves tonumberbecause that’s the only member present in both unions.- If there’s no overlap, the intersection becomes
never.
Type guards
A type guard is any check that helps TypeScript narrow a union type to something more specific so you can safely use type-specific operations.
typeof type guard (primitives)
type Combinable = string | number;
function add(a: Combinable, b: Combinable){
if(typeof a === 'string' || typeof b === 'string') { // this is called a typeguard, using typeof.
return a.toString() + b.toString();
}
return a+b;
}Here:
typeof ... === 'string'narrows that value tostring.- Otherwise, both are treated as
number, soa + bis numeric addition.
in type guard (object shapes)
When you have a union of object types, typeof won’t help. Instead, use property existence checks with in.
type Admin = {
name: string;
privileges: string[]
}
type Employee = {
name: string;
startDate: Date;
}
type ElevatedEmployee = Admin & Employee
type UnknownEmployee = Employee | Admin;
function printEmployeeInformation(emp : UnknownEmployee){
console.log(emp.name)
// console.log(emp.privileges) // errors because ooptional, only admin has it.
// therefore correction:
if('privileges' in emp){// can be used with interfaces.
console.log(emp.privileges);
}
if('startDate' in emp){
console.log(emp.startDate);
}
}
printEmployeeInformatioN({name:'anu', startDate: new Date()});Why this matters:
UnknownEmployeecould be either type, so accessingprivilegesdirectly is unsafe.'privileges' in empnarrowsemptoAdmin(or at least to “something that has privileges”).
instanceof type guard (classes)
For class-based unions, instanceof is usually the cleanest runtime-safe narrowing.
class Car{
drive(){
console.log('Driving')
}
}
class Truck {
drive() {
console.log('Driving truck')
}
loadCargo(amount){
console.log(`loading cargo... ${amount}`)
}
}
const v1 = new Car();
const v2 = new Truck();
function useVehicle(vehicle: Vehicle){
vehicle.drive();
//if( 'loadCargo' in vehicle){ // another way of doing this:
if(vehicle instanceof Truck){
vehicle.loadCargo(1000);
}
}
type Vehicle = Car | Truck
Notes:
instanceofworks becauseTruckexists at runtime (it’s a real constructor function).inis also valid here, butinstanceofis often clearer for class hierarchies.
Discriminated unions
A discriminated union is a pattern that makes type narrowing easy and scalable by adding a shared “tag” property with literal values (often type).
interface Bird {
flyingSpeed: number;
type: 'bird';
}
interface Horse {
runningSpeed: number;
type: 'horse';
}
type Animal = Bird | Horse;
/*
The above code is discriminated union, making it easier to discriminate types by using literal properties, or other properties that we know the value of.
*/
function moveAnimal(animal: Animal){
let speed;
switch(animal.type){
case 'bird': speed = animal.flyingSpeed;break;
case 'horse':speed = animal.runningSpeed;break;
}
console.log('Moving at speed: ' + speed)
}
moveAnimal({type:'bird', flyingSpeed: 10});Why it’s great:
- Adding more variants (
Fish,Snake, etc.) scales better than piling oninchecks. switch(animal.type)becomes a central place to handle each variant.- You can also do “exhaustiveness checks” (ensuring every case is handled) as your union grows.
Type casting (type assertions)
Type assertions are for when TypeScript can’t know a specific type, but you (as the author) know it.
Important: type assertions do not change runtime behavior—no conversion happens. They only affect the type checker.
//const paragraph = document.getElementById('user-input')! //
const userInputElement = document.getElementById('user-input')! as HTMLInputElement// alternative since react also uses anglebrackets.
// one way of doing this.<HTMLInputElement>document.getElementById('user-input')!;
html element is the type, since ts can't know the specific type.
userInputElement.value = 'Hi there!'; //throws an error without typecasting since htmlElement doesn't have a property value.
//alternative to exclamationmark: (userInputElement ad HTMLInputElement).value = 'Hi there!'
What’s going on:
getElementByIdreturns a generalHTMLElement | null(or similar), because it can’t know what element you’re selecting.!is the non-null assertion (“trust me, it exists”). Use it carefully—if the element is missing, your code will still crash at runtime.as HTMLInputElementnarrows the element type so.valueis allowed.
Safer alternatives in real code:
- Check for null (
if (!el) return) before using it. - Narrow by checking
tagNameor usingquerySelector<HTMLInputElement>(...)in some setups.
Index signatures (index properties)
Index signatures let you define “this object can have unknown property names” with a consistent value type.
interface ErrorContainer { /* {email: 'not a valid email', username: 'Must start with a character'*/
id:string;
[prop: string]: string; // don't know name or property count, but all properties added must be a property name as string and value as string. Index type.
}
const errorBag: ErrorContainer = {
email: 'Not a valid email.'
12: 'Not a valid number' // number can be interpreted as string, but not the other way around.
}Why you’d use this:
- Validation errors are often dynamic:
email,username,password, etc. - You don’t want to predefine every possible key, but you want to enforce that every message is a
string.
Two important constraints:
- Once you declare
[prop: string]: string, all string-keyed properties must have string values (that’s why this works well for error message “bags”). - Numeric keys are allowed because JS coerces them to strings internally (
errorBag[12]becomeserrorBag["12"]).
Function overloads
Overloads let you provide multiple call signatures so TypeScript can infer a more precise return type depending on the inputs.
type Combinable = number | string;
function add(a: number, b: number):number // function overload.
function add(a: string, b: string):string
function add(a: Combinable, b: Combinable) {
if(typeof a ==='string' || typeof b === 'string') {
return a.toString() + b.toString()
}
return a+b
}
const result = add(1,5);// result is of type combinable. It doesn't know that result is a number. This limits methods that we can use on it.
result.split(' ') // or result.toString() for a number
What overloads do (conceptually):
- They tell TS: “if you call
addwith two numbers, you get anumber; if you call it with two strings, you get astring.” - The last
add(...)is the implementation signature and must be compatible with all overload signatures.
Practical nuance:
- If you want to support mixed calls like
add("a", 1), you’d add an overload for that too—or disallow it by design.
Optional chaining
Optional chaining (?.) is for safely accessing properties when something might be null/undefined.
fetchedUserData = {
id: 'u1',
name: 'Max',
job: { title: 'CEO', description: 'My own company'}
}
console.log(fetchedUserData?.job?.title);
How it behaves:
- If
fetchedUserDataisnull/undefined, the expression returnsundefinedinstead of throwing. - If
jobis missing, it stops and returnsundefined. - This is ideal for DB/API data where optional nested objects are common.
Nullish coalescing
The nullish coalescing operator (??) picks a fallback only when the left side is null or undefined.
//const userInput = null; // assume a failed DB call.
const userInput = '';
//const storedData = userInput || 'Default' // chooses default even with empty string
const storedData = userInput ?? 'Default';
console.log(storedData);Why ?? is different from ||:
||treats all falsy values ("",0,false) as “missing.”??treats onlynullandundefinedas “missing.”
So "" ?? "Default" stays "", which is often what you want for user-entered strings.
Generics
Generics are a TypeScript feature (compile-time only) that let you write reusable code while preserving type information.
Built-in generics: arrays and promises
-
Array<T>is a generic type; it needs the element type.any[]is the same concept asArray<any>
-
Promise<T>is generic too:Promise<unknown>means “this resolves to something, but we’ll narrow later.”
Creating your own generic functions
Non-generic merge loses detail:
function merge(objA: object, objB: object){
return Object.assign(objA,objB);
}
//console.log(merge({name: 'max'},{age:40}));// works fine.
mergedObj = merge({name: 'max'},{age:40}) // cannot find name or age here.
Generic merge preserves detail:
function merge<T, U>(objA: T, objB: U){
return Object.assign(objA,objB);
}// typescript detects that it returns t & u
Why it improves things:
- TS infers
TandUfrom the arguments. - The return type becomes
T & U, so the merged object “has both sets of properties” from the type system’s perspective.
Constraining generics
To avoid passing non-objects:
function merge<T extends object, U extends object>(objA: T, objB: U){
return Object.assign(objA,objB);
}// typescript detects that it returns t & u
This answers your question:
How might we ensure that a data structure has a particular property or method in TS?
Use a generic constraint (extends) against an interface/type that includes that property/method.
Example: require a .length:
interface Lengthy{
length: number
}
function counAndDescribe<T extends Lengthy>(element: T){
let descriptionText = 'got no value.';
if(element.length>0) descriptionText = `Got ${element.length} element(s)`
return [element, descriptionText]
} // extending lengthy means that we expect something that has a length property.This guarantees element.length is valid, regardless of whether element is a string, array, custom object, etc.
keyof constraints
keyof produces a union of valid keys from a type. In generics, it’s commonly used to ensure you can only pass keys that actually exist on the object.
function extractAndConvert<T extends object, U extends keyof T (obj:T,key: U){
return obj[key]; //throws error, since we don't know that obj has a key property.
}
extractAndConvert({name:'Max'}, 'name');What keyof is used for:
- Prevents invalid property access like
extractAndConvert({ name: "Max" }, "age"). - Makes dynamic indexing type-safe (
obj[key]) becausekeyis guaranteed to be a real key ofobj.
(Separately: the snippet has a small syntax issue in the generic parameter list, but the concept you’re capturing is exactly what keyof is for.)
Generic classes + constraints (and why primitives matter)
class DataStorage<T extends string | number | boolean>{
private data: T[] = [];
addItem(item: T){
this.data.push(item);
}
removeItem(item:T){
if(this.data.indexOf(item)===-1) return;
this.data.splice(this.data.indexOf(item),1);
}
getItems(){
return [...this.data];
}
}
const textStorage = new DataStorage<string>();
textStorage.addItem('Max'); // works
// textStorage.addItem(1) // fails.
//one Issue
const objStorage = new DataStorage<object>();
objStorage.addItem({name:'Max'})
objStorage.addItem({name:'Manu'})
//...
objStorage.removeItem({name: 'Max'});
console.log(objStorage.getItems()); // this still gives max. because indexOf cannot search for objects properly. it returns -1Why constrain to primitives?
-
indexOfuses reference equality for objects.{name:'Max'}is not the same reference as another{name:'Max'}, soindexOfcan’t find it.
-
With primitives (
string | number | boolean), equality behaves as you expect.
If you want object storage, typical solutions are:
- store objects with stable IDs and remove by ID
- accept a comparator function
- use a
Mapkeyed by ID
Generic utility types (intro)
Utility types are built-in helpers that transform other types (e.g., making properties optional, readonly, etc.).
Your snippet sets up a common situation: you want to build an object step-by-step but still return a fully-typed result.
interface CourseGoal{
title:string;
description: string;
completeUntil: Date;
}
function createCourseGoal (title:string, description: string, date:Date): CourseGoal {
return {title, description, completeUntil:date};
}The “generic utility types” direction here typically leads to patterns like:
- constructing a
Partial<CourseGoal>first, - filling it,
- then returning it as
CourseGoal.