If you know JavaScript, TypeScript is JavaScript plus a type checker — same runtime, same npm packages, same Node.js. If you know Python or Java, TypeScript is the parts that feel familiar plus some JS-isms. This lesson covers what you need to read and write CDK stacks fluently.
Setup
npm install -g typescript ts-node
tsc --version
mkdir my-infra && cd my-infra
npm init -y
npm install typescript @types/node --save-dev
npx tsc --init # creates tsconfig.json
Run TypeScript files without a separate compilation step using ts-node or tsx.
Basic Types
const name: string = "kube"
const port: number = 8080
const enabled: boolean = true
let count = 0 // type inferred as number
// Arrays
const nodes: string[] = ["node1", "node2"]
const ports: Array<number> = [80, 443]
// Tuple — fixed-length heterogeneous array
const pair: [string, number] = ["web", 80]
// Null handling
let maybeNull: string | null = null
maybeNull = "ok"
// Any — escape hatch; use sparingly
const unknown: unknown = fetchData() // prefer unknown to any; forces narrowing
Interfaces and Type Aliases
These describe the shape of objects. Both are erased at runtime — purely static.
// Interface
interface VpcConfig {
cidr: string
maxAzs: number
natGateways?: number // optional (?)
readonly vpcName: string // readonly
}
// Type alias
type InstanceSize = "small" | "medium" | "large" // union literal type
type Environment = "dev" | "staging" | "prod"
// Use them
const cfg: VpcConfig = { cidr: "10.0.0.0/16", maxAzs: 3, vpcName: "main" }
const size: InstanceSize = "medium"
Prefer interface for object shapes (extensible via declaration merging); type for unions, intersections, and aliases.
Classes
CDK constructs are classes. Understanding class inheritance is essential.
class Stack {
readonly account: string
readonly region: string
protected resources: string[] = []
constructor(account: string, region: string) {
this.account = account
this.region = region
}
addResource(id: string): void {
this.resources.push(id)
}
}
class DatabaseStack extends Stack {
constructor(account: string, region: string, private dbName: string) {
super(account, region)
this.addResource(`rds-${dbName}`)
}
}
const db = new DatabaseStack("123456789", "us-east-1", "orders")
db.account // "123456789"
db.resources // Error! protected — only accessible in subclass
Generics
Generics make code reusable across types while keeping type safety.
// Generic function
function getFirst<T>(items: T[]): T | undefined {
return items[0]
}
const s = getFirst(["a", "b"]) // type: string | undefined
const n = getFirst([1, 2, 3]) // type: number | undefined
// Generic interface
interface TaggedResource<T> {
resource: T
tags: Record<string, string>
}
// Constraint — T must have at least this shape
function tagName<T extends { id: string }>(item: T): string {
return `resource-${item.id}`
}
You encounter generics constantly in CDK: Stack, App, Construct are all generic classes internally. Understanding them prevents copy-paste confusion.
Enums and Const Enums
// String enum — most common in CDK
enum RemovalPolicy {
DESTROY = "destroy",
RETAIN = "retain",
SNAPSHOT = "snapshot",
}
// Use
const policy = RemovalPolicy.RETAIN
// Const enum (fully inlined at compile time)
const enum LogLevel { DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3 }
CDK uses string enums heavily for resource properties — ec2.InstanceClass.T4G, ecs.CpuArchitecture.ARM64, etc.
Async / Await and Promises
Node.js is single-threaded and async. Pulumi's output system and CDK custom resources both rely on Promises.
// Promise-based
function fetchSecretAsync(secretId: string): Promise<string> {
return client.getSecretValue({ SecretId: secretId }).promise()
}
// async/await — syntactic sugar over Promises
async function deployStack(): Promise<void> {
const secret = await fetchSecretAsync("db-password")
const stack = await cf.createStack({ StackName: "myapp", Parameters: [/* ... */] }).promise()
console.log(`Stack ${stack.StackId} creating`)
}
// Error handling — same as synchronous try/catch
async function safeGet(id: string): Promise<string | null> {
try {
return await fetchSecretAsync(id)
} catch (err) {
console.error(err)
return null
}
}
// Concurrent
const [vpcId, securityGroupId] = await Promise.all([
fetchVpc(),
fetchSecurityGroup(),
])
Modules and Imports
// Named export
export interface Config { region: string; account: string }
export function defaultConfig(): Config { return { region: "us-east-1", account: "123456" } }
// Default export
export default class App { /* ... */ }
// Import
import { Config, defaultConfig } from './config'
import App from './app'
import * as cdk from 'aws-cdk-lib' // namespace import
import { Stack, StackProps } from 'aws-cdk-lib' // named imports
CDK uses namespace imports heavily: import * as ec2 from 'aws-cdk-lib/aws-ec2'. Each service lives in its own sub-path.
The tsconfig.json That Matters
CDK projects use a near-standard config:
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"moduleResolution": "node",
"lib": ["ES2022"],
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitReturns": true,
"esModuleInterop": true,
"declaration": true,
"outDir": "dist"
},
"include": ["lib/**/*.ts", "bin/**/*.ts", "test/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
strict: true enables all strict type checks. Always. The number of production bugs prevented by strictNullChecks alone is hard to overstate.
Utility Types You Will See in CDK
| Utility type | Meaning | CDK example |
|---|---|---|
Partial<T> | All properties optional | CDK prop override bags |
Required<T> | All properties required | Validation helpers |
Readonly<T> | All properties readonly | Immutable config |
Record<K, V> | Dictionary / map type | Tag maps, environment maps |
Pick<T, K> | Subset of properties | Narrow prop types in sub-constructs |
Omit<T, K> | All except named keys | Remove parent props from child interface |
What You Now Know
With types, interfaces, classes, generics, and async/await in hand you can read any CDK construct library source. The next lesson puts this to work — creating an actual CDK application.