TypeScript for LLD
TypeScript is JavaScript plus one habit: you write down the shape of every value, and a
checker reads your code before it runs. That is the whole language. There is no new
runtime and nothing new to deploy. Your .ts file is turned into plain .js, and the
types are deleted on the way out.
That one habit is why TypeScript is a good language to learn design in. An interface in JavaScript is a promise you keep in your head. In TypeScript it is a line of code the compiler holds you to.
This page is the 20 minutes of TypeScript the rest of the series stands on. If you can read JavaScript, you can read everything below. If you cannot yet, start at JavaScript basics and come back.
A function takes a price and a quantity and multiplies them. In JavaScript, what happens if someone calls it with the string '120' and the number 2? What would you want to happen instead?
Set it up once
You need Node and two packages. typescript is the checker, tsx runs a .ts file
directly so you do not have to compile by hand while you are learning.
mkdir lld-practice && cd lld-practice
npm init -y
npm install --save-dev typescript tsx
npx tsc --initOpen the tsconfig.json that last command created and make sure these two are on. Every
snippet in this series is checked with both.
{
"compilerOptions": {
"strict": true,
"noImplicitOverride": true
}
}Then put any snippet from these pages in a file and run it with npx tsx bank.ts. If you
do not want to install anything, paste it into the playground at typescriptlang.org/play.
Three comment styles repeat on every page. // 240 after a console.log is what that
line prints. A line starting with // > is what the line above it printed. And
// Error:, on a line or directly under it, marks code the compiler refuses, which is
usually the point of the example.
Annotations, and when to skip them
You annotate a variable with a colon and a type. Most of the time you do not have to, because TypeScript works the type out from the value. That is called inference.
let price: number = 120
let dish = 'Masala dosa'
price = 135
dish = 42 // Error: Type 'number' is not assignable to type 'string'.dish was never annotated. TypeScript saw a string go in and decided it is a string for
life. The rule of thumb that works: annotate function parameters and return types, let
everything else be inferred.
function total(price: number, quantity: number): number {
return price * quantity
}
console.log(total(120, 2)) // 240
total('120', 2)
// Error: Argument of type 'string' is not assignable to parameter
// of type 'number'.In JavaScript that last call quietly returns 240 because of string coercion, and the day
someone adds instead of multiplies you get '1202' on a customer’s bill. TypeScript stops
it at the keyboard.
Describing an object
An object type lists the properties and what each one holds. A ? marks a property that
may be missing.
type Dish = {
name: string
price: number
veg: boolean
note?: string
}
const dosa: Dish = { name: 'Masala dosa', price: 120, veg: true }
const idli: Dish = { name: 'Idli', price: 60 }
// Error: Property 'veg' is missing in type
// '{ name: string; price: number; }' but required in type 'Dish'.
console.log(dosa.note) // undefinedThere is a second way to write the same thing, and it is the one this series uses most.
An interface describes what something can do.
interface Notifier {
send(message: string): void
}
const sms: Notifier = {
send(message) {
console.log(`SMS: ${message}`)
},
}
sms.send('Your order is on the way')
// > SMS: Your order is on the wayvoid means the function returns nothing you should use. Notice that message inside
the object needed no annotation. The interface already said it is a string.
They overlap almost completely and people argue about it more than it deserves. The
convention on these pages: interface for a behaviour a class promises to provide,
type for plain data and for the “one of these” types in the next section.
One of several: unions
A union type says a value is one of a fixed set. With string literals, it replaces most of what you would use an enum for.
type OrderStatus = 'placed' | 'cooking' | 'delivered'
let status: OrderStatus = 'placed'
status = 'cooking'
status = 'cancelled'
// Error: Type '"cancelled"' is not assignable to type 'OrderStatus'.
function describe(id: number | string): string {
if (typeof id === 'number') {
return `Order #${id.toFixed(0)}`
}
return `Order ${id.toUpperCase()}`
}
console.log(describe(42)) // Order #42
console.log(describe('ab12')) // Order AB12Look at describe. Inside the if, TypeScript knows id is a number, so toFixed is
allowed. After it, the only thing left is a string, so toUpperCase is allowed. This is
called narrowing, and you will lean on it in the State
and Polymorphism pages.
Collections and the angle brackets
number[] is an array of numbers. For anything else that holds values, the type of the
contents goes in angle brackets. Those are generics, and reading them is easier than the
name suggests: Map<string, number> is “a Map from string to number”.
const prices: number[] = [120, 60, 40]
const stock = new Map<string, number>()
stock.set('dosa', 12)
console.log(stock.get('dosa')) // 12
console.log(stock.get('vada')) // undefined
function first<T>(items: T[]): T | undefined {
return items[0]
}
console.log(first(prices)) // 120
console.log(first(['idli', 'vada'])) // idlifirst<T> reads as “for whatever type T you hand me an array of, I give back a T”. You
wrote the function once and it is correctly typed for numbers, strings and everything
else. That T | undefined is honest about the empty array.
The compiler will not let you forget undefined
stock.get can come back empty, and with strict on, TypeScript makes you deal with
that before you do arithmetic on it.
const stock = new Map<string, number>([['dosa', 12]])
const left = stock.get('vada')
console.log(left + 1) // Error: 'left' is possibly 'undefined'.
console.log((left ?? 0) + 1) // 1?? means “use the right side if the left is null or undefined”. Half of the production
bugs in a JavaScript codebase are this one line, missing.
A class, with types on it
A class bundles data with the functions that work on it. The TypeScript additions are small: fields are declared with their types at the top, and methods are annotated like any other function.
class Cart {
items: string[] = []
add(item: string): void {
this.items.push(item)
}
get count(): number {
return this.items.length
}
}
const cart = new Cart()
cart.add('Masala dosa')
cart.add('Filter coffee')
console.log(cart.count) // 2get count() is a getter. It is a method that is read like a property, so you write
cart.count and not cart.count().
This Cart has a problem you may have spotted: anyone can reach in and do
cart.items = []. Fixing that is the first design idea in the series, and it is where
Encapsulation starts.
A spell checker does not write the essay. It underlines the word you got wrong while
you are still typing, not after you have posted it. The red squiggle under
total('120', 2) is the same thing for code.
let price: number(price: number): numbertype Dish = { ... }note?: stringinterface Notifier { ... }'placed' | 'cooking'Map<string, number>function first<T>(...)left ?? 0Read it back
Cover the right side and say the type out loud before you reveal it.
Checkpoint
1. You write let city = "Pune" with no annotation. What is the type of city?
2. A Map<string, number> is asked for a key it does not have. What does TypeScript say the result is?
3. What happens to your interfaces and type annotations when the code actually runs?
TypeScript is JavaScript with the shapes written down. I annotate function parameters and return types and let the rest be inferred. I describe plain data with a type, behaviour with an interface, and a fixed set of options with a union of string literals. Generics are just the type of what a container holds, so Map of string to number. Strict mode forces me to handle undefined before I use a value. And all of it is erased at compile time, so types protect my code from me, not from bad data arriving at runtime.
