Curated questions covering types, interfaces, generics, decorators, enums, utility types, mapped types, and TypeScript configuration.
TypeScript is a statically typed superset of JavaScript that compiles to plain JavaScript. Benefits: catch errors at compile time, better IDE support (autocomplete, refactoring), self-documenting code, safer large-scale applications, and first-class support for modern JavaScript features.
interface User { name: string; age: number; }
interface User { email: string; } // merged - OK
type ID = string | number; // union - only with type
type Point = { x: number } & { y: number }; // intersection
Generics allow writing reusable, type-safe code that works with multiple types. They act as type placeholders resolved at usage time.
function identity<T>(value: T): T { return value; }
identity<string>("hello"); // T = string
identity<number>(42); // T = number
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
let a: unknown = "hello";
if (typeof a === "string") a.toUpperCase(); // must narrow first
function fail(msg: string): never {
throw new Error(msg); // never returns
}
Utility types are built-in generic types that transform existing types.
interface User { id: number; name: string; email: string; }
type PartialUser = Partial<User>;
type UserPreview = Pick<User, "id" | "name">;
type UserWithoutEmail = Omit<User, "email">;
type StringMap = Record<string, string>;
Type narrowing refines a broad type to a more specific one within a conditional block using control flow analysis.
function process(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase(); // narrowed to string
}
return value.toFixed(2); // narrowed to number
}
function handle(err: unknown) {
if (err instanceof Error) console.log(err.message);
}
Enums define a set of named constants. TypeScript supports numeric enums (default), string enums, and const enums.
enum Direction { Up, Down, Left, Right } // 0,1,2,3
enum Status { Active = "ACTIVE", Inactive = "INACTIVE" } // string enum
const enum Color { Red, Green, Blue } // inlined at compile time
interface A { x: number; }
interface B extends A { y: number; } // B has x and y
type C = { x: number } & { y: number }; // same result
type Conflict = { x: string } & { x: number }; // x becomes never
Decorators are special declarations that attach metadata or modify classes, methods, properties, or parameters. Widely used in Angular and NestJS. Enable with experimentalDecorators in tsconfig.
function Log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${key} with`, args);
return original.apply(this, args);
};
return descriptor;
}
class Service {
@Log
greet(name: string) { return `Hello, ${name}`; }
}
type StringOrNumber = string | number;
type AdminUser = User & { adminLevel: number };
function format(val: string | number): string {
return typeof val === "string" ? val : val.toString();
}
readonly prevents a property from being reassigned after initialization. It applies at compile time only. Use Readonly
interface Config {
readonly apiUrl: string;
readonly timeout: number;
}
const config: Config = { apiUrl: "/api", timeout: 3000 };
// config.apiUrl = "/new"; // Error: cannot assign to readonly property
Mapped types create new types by transforming each property of an existing type. They are the foundation of utility types like Partial, Readonly, and Record.
type Nullable<T> = { [K in keyof T]: T[K] | null };
type Optional<T> = { [K in keyof T]?: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // remove readonly
interface User { id: number; name: string; }
type NullableUser = Nullable<User>; // { id: number|null; name: string|null }
Conditional types select a type based on a condition: T extends U ? X : Y. They enable powerful type-level logic.
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type NonNullable<T> = T extends null | undefined ? never : T;
type Flatten<T> = T extends Array<infer U> ? U : T;
const palette = {
red: [255, 0, 0],
green: "#00ff00",
} satisfies Record<string, string | number[]>;
// palette.red is still number[], not string | number[]
palette.red.map(x => x); // OK - type preserved
A discriminated union is a union of types that share a common literal property (discriminant). TypeScript uses the discriminant to narrow the type in switch/if statements.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.side ** 2;
}
}
keyof produces a union of all property keys of a type. Combined with generics, it enables type-safe property access.
interface User { id: number; name: string; email: string; }
type UserKeys = keyof User; // "id" | "name" | "email"
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]; // fully type-safe
}
In TypeScript, typeof can be used in type positions to capture the type of a variable or function. Different from JavaScript runtime typeof.
const config = { host: "localhost", port: 3000 };
type Config = typeof config; // { host: string; port: number }
function createUser(name: string, age: number) { return { name, age }; }
type User = ReturnType<typeof createUser>; // { name: string; age: number }
Template literal types build string types by combining string literals, similar to JavaScript template literals.
type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`; // "onClick" | "onFocus" | "onBlur"
type CSSProperty = `${string}-${string}`;
const prop: CSSProperty = "background-color"; // OK
Enabling "strict": true in tsconfig.json activates a set of strict type-checking options.
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ESNext"
}
}
Declaration merging combines multiple declarations with the same name into a single definition. Works with interfaces, namespaces, and enums.
interface Window { myPlugin: () => void; }
// Merges with the built-in Window interface
// Now window.myPlugin is type-safe
// Augmenting a module
declare module "express" {
interface Request { user?: { id: number; role: string }; }
}
abstract class Animal {
abstract speak(): string;
breathe() { return "breathing"; } // concrete method
}
interface Flyable { fly(): void; }
interface Swimmable { swim(): void; }
class Duck extends Animal implements Flyable, Swimmable { ... }
Index signatures allow defining types for objects with dynamic keys.
interface StringMap { [key: string]: string; }
const headers: StringMap = { "Content-Type": "application/json" };
interface Config {
timeout: number;
[key: string]: number; // all values must be number
}
infer is used inside conditional types to capture and reuse a type within the condition. It is how ReturnType and Parameters utility types are implemented.
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function greet(name: string): string { return `Hello ${name}`; }
type R = ReturnType<typeof greet>; // string
type P = Parameters<typeof greet>; // [name: string]
Module augmentation extends existing module types without modifying the original source. Useful for adding types to third-party libraries.
// Extend Express Request type
import "express";
declare module "express" {
interface Request {
user?: { id: number; role: string };
}
}
// Now req.user is type-safe in all Express handlers
const input = document.getElementById("name") as HTMLInputElement;
const value = (input as HTMLInputElement).value;
// Double assertion for incompatible types
const x = "hello" as unknown as number; // unsafe!
type T = string | number | boolean;
type NoBoolean = Exclude<T, boolean>; // string | number
type OnlyString = Extract<T, string>; // string
type NonNullable<T> = Exclude<T, null | undefined>;
// Interface call signature
interface Formatter {
(value: string, options?: object): string;
}
// Type alias
type Formatter = (value: string, options?: object) => string;
// Both work the same way
const fmt: Formatter = (v) => v.trim();
function sum(nums: readonly number[]): number {
// nums.push(1); // Error: push does not exist on readonly
return nums.reduce((a, b) => a + b, 0);
}
const arr: ReadonlyArray<number> = [1, 2, 3];
// Namespace (legacy)
namespace Utils {
export function format(s: string) { return s.trim(); }
}
Utils.format("hello");
// Module (modern)
export function format(s: string) { return s.trim(); }
// Overloads
function process(x: string): string;
function process(x: number): number;
function process(x: string | number): string | number {
return typeof x === "string" ? x.toUpperCase() : x * 2;
}
let x = "hello"; // widened to string
const y = "hello"; // literal type "hello"
const z = "hello" as const; // literal type "hello"
const arr = [1, 2, 3] as const; // readonly [1, 2, 3]
interface User { id: number; name: string; email: string; password: string; }
type PublicUser = Omit<User, "password">; // id, name, email
type UserCredentials = Pick<User, "email" | "password">; // email, password
function createUser(name: string, age: number, role: string) {
return { name, age, role };
}
type Params = Parameters<typeof createUser>; // [string, number, string]
type Result = ReturnType<typeof createUser>; // { name: string; age: number; role: string }
class User {
constructor(public name: string, public age: number) {}
}
type UserInstance = InstanceType<typeof User>; // User
type UserParams = ConstructorParameters<typeof User>; // [string, number]
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number
type C = Awaited<string>; // string (non-Promise passthrough)
async function fetchUser(): Promise<User> { ... }
type FetchResult = Awaited<ReturnType<typeof fetchUser>>; // User
import type { User } from "./types"; // type-only, erased at runtime
import { createUser } from "./utils"; // value import, kept
// TypeScript 5.0+ inline type imports
import { type User, createUser } from "./module";
const config = {
port: 3000,
host: "localhost"
} as const; // { readonly port: 3000; readonly host: "localhost" }
const config2 = {
port: 3000,
host: "localhost"
} satisfies { port: number; host: string };
// port is still 3000 (literal), not number
// tsconfig.json
{
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/ui" }
]
}
// With strictNullChecks
function getCity(user: User | null): string | undefined {
return user?.address?.city; // optional chaining
}
// tsconfig.json
{
"compilerOptions": {
"paths": { "@/*": ["./src/*"] }
}
}
// Usage
import { formatDate } from "@/utils/date"; // instead of "../../../utils/date"
The using declaration (TypeScript 5.2, ECMAScript proposal) automatically disposes resources when they go out of scope, similar to C# using or Python with. The resource must implement Symbol.dispose.
class DatabaseConnection {
[Symbol.dispose]() { this.close(); }
close() { console.log("Connection closed"); }
}
{
using conn = new DatabaseConnection();
// use conn...
} // conn.close() called automatically here
Const type parameters (TS 5.0) infer literal types instead of widened types, similar to as const but for generic functions.
// Without const: T inferred as string[]
function identity<T>(arr: T): T { return arr; }
identity(["a", "b"]); // string[]
// With const: T inferred as ["a", "b"]
function identity<const T>(arr: T): T { return arr; }
identity(["a", "b"]); // readonly ["a", "b"]
// CommonJS .d.ts
declare module "my-lib" {
export function helper(): void;
export default class MyLib { ... }
}
// ESM .d.ts
export function helper(): void;
export default class MyLib { ... }
enum Color { Red, Green, Blue } // compiled to JS object
const enum Direction { Up, Down } // inlined: Direction.Up becomes 0
// Reverse mapping (numeric enums only)
Color[0]; // "Red"
abstract class Shape {
abstract area(): number; // must implement
toString() { return `Area: ${this.area()}`; } // concrete
}
class Circle extends Shape {
constructor(private r: number) { super(); }
area() { return Math.PI * this.r ** 2; } // required
}
// Type guard
function isString(val: unknown): val is string {
return typeof val === "string";
}
// Assertion function
function assertString(val: unknown): asserts val is string {
if (typeof val !== "string") throw new Error("Not a string");
}
assertString(value); // value is string after this line
The accessor keyword (TS 4.9+) is shorthand for defining a class field with auto-generated getter and setter. It is cleaner than manually writing get/set pairs.
// Traditional get/set
class User {
private _name = "";
get name() { return this._name; }
set name(v: string) { this._name = v; }
}
// accessor keyword (TS 4.9+)
class User {
accessor name = ""; // auto-generates get/set
}
Explore 500+ free tutorials across 20+ languages and frameworks.