TypeScript 5 3
Release Overview
TypeScript 5.3 was released on November 16, 2023, bringing significant improvements to module imports, type narrowing, and build performance with isolated declarations.
Key Metrics:
- Release Date: November 16, 2023
- Major Focus: Import attributes, enhanced narrowing, isolated declarations
- Breaking Changes: Minimal
- Performance: Up to 50% faster declaration emit with
--isolatedDeclarations
Import Attributes
Landmark Feature: Support for ECMAScript import attributes proposal, enabling type-safe imports with runtime validation.
The Problem It Solves
Before Import Attributes: No standardized way to specify import metadata for module types like JSON, CSS, or WebAssembly.
// Before - no type safety or validation
import config from "./config.json";
// TypeScript infers type, but no runtime validation
// Build tools need separate configuration
// Potential runtime failures:
// - Wrong file type loaded
// - Missing file not caught at build time
// - No validation of import intent
With Import Attributes
Solution: Explicitly declare import type with attributes, enabling both compile-time and runtime validation.
// After - explicit type assertion with validation
import config from "./config.json" with { type: "json" };
// ✅ TypeScript validates JSON structure
// ✅ Runtime validates file is JSON
// ✅ Build tools optimize JSON loading
// ✅ Bundlers tree-shake JSON imports
// Type safety preserved
type Config = typeof config;
// Type: { host: string; port: number; ... }
// Invalid attribute caught at compile time
import data from "./data.csv" with { type: "json" };
// ❌ Error: CSV file with JSON attribute mismatch
Real-World Application: Configuration Management
Type-safe configuration loading with runtime validation:
// config/database.json
{
"host": "localhost",
"port": 5432,
"database": "production",
"ssl": true
}
// app/database.ts
import dbConfig from "../config/database.json" with { type: "json" };
// ✅ Type inferred from JSON structure
type DBConfig = typeof dbConfig;
// Type: {
// host: string;
// port: number;
// database: string;
// ssl: boolean;
// }
function connectDatabase() {
const connection = createConnection({
host: dbConfig.host, // ✅ Type-safe access
port: dbConfig.port, // ✅ Number type preserved
database: dbConfig.database,
ssl: dbConfig.ssl, // ✅ Boolean type preserved
});
// Would error if config structure changes
// console.log(dbConfig.invalidField); // ❌ Error
}Real-World Application: Feature Flags
Load feature flags with guaranteed JSON format:
// features/flags.json
{
"darkMode": true,
"betaFeatures": false,
"analytics": true,
"notifications": {
"email": true,
"push": false
}
}
// app/features.ts
import flags from "./flags.json" with { type: "json" };
// ✅ Runtime guarantees JSON format
// ✅ Type safety for nested objects
type FeatureFlags = typeof flags;
// Type: {
// darkMode: boolean;
// betaFeatures: boolean;
// analytics: boolean;
// notifications: {
// email: boolean;
// push: boolean;
// };
// }
function isFeatureEnabled<K extends keyof FeatureFlags>(
feature: K
): FeatureFlags[K] {
return flags[feature]; // ✅ Type-safe, validated at runtime
}
// Usage with autocomplete
if (isFeatureEnabled("darkMode")) {
// ✅ Type: boolean
enableDarkTheme();
}
if (isFeatureEnabled("notifications")) {
// ✅ Type: { email: boolean; push: boolean }
const emailEnabled = flags.notifications.email;
}Real-World Application: Localization Data
Type-safe internationalization with validated JSON:
// locales/en.json
{
"common": {
"welcome": "Welcome",
"logout": "Logout"
},
"errors": {
"notFound": "Page not found",
"unauthorized": "Access denied"
}
}
// locales/id.json
{
"common": {
"welcome": "Selamat datang",
"logout": "Keluar"
},
"errors": {
"notFound": "Halaman tidak ditemukan",
"unauthorized": "Akses ditolak"
}
}
// i18n/translations.ts
import en from "../locales/en.json" with { type: "json" };
import id from "../locales/id.json" with { type: "json" };
// ✅ Both locales must have same structure
type TranslationKeys = typeof en;
const translations: Record<string, TranslationKeys> = {
en,
id, // ✅ Type-checked against en structure
};
function translate(
locale: keyof typeof translations,
path: string
): string {
const keys = path.split(".");
let result: any = translations[locale];
for (const key of keys) {
result = result[key];
}
return result as string;
}
// Type-safe translation keys
const message = translate("en", "common.welcome");
// ✅ Returns: "Welcome"
const error = translate("id", "errors.notFound");
// ✅ Returns: "Halaman tidak ditemukan"
Real-World Application: API Mock Data
Load test fixtures with guaranteed structure:
// tests/fixtures/users.json
[
{
id: 1,
name: "Alice",
email: "alice@example.com",
role: "admin",
},
{
id: 2,
name: "Bob",
email: "bob@example.com",
role: "user",
},
];
// tests/user.test.ts
import mockUsers from "./fixtures/users.json" with { type: "json" };
// ✅ Type inferred from JSON array structure
type MockUser = (typeof mockUsers)[number];
// Type: {
// id: number;
// name: string;
// email: string;
// role: string;
// }
describe("User API", () => {
it("should fetch user by ID", async () => {
// Use typed mock data in tests
const expectedUser = mockUsers[0];
// ✅ Type: MockUser
const response = await fetchUser(expectedUser.id);
expect(response).toEqual(expectedUser);
expect(response.name).toBe("Alice"); // ✅ Type-safe
expect(response.role).toBe("admin"); // ✅ Type-safe
});
it("should filter admin users", () => {
const admins = mockUsers.filter((u) => u.role === "admin");
// ✅ Type: MockUser[]
expect(admins).toHaveLength(1);
expect(admins[0].name).toBe("Alice");
});
});switch (true) Narrowing
Feature: TypeScript now narrows types inside switch (true) statements based on case conditions.
Example
function processValue(value: string | number | boolean) {
switch (true) {
case typeof value === "string":
// ✅ Narrowed to: string
console.log(value.toUpperCase());
break;
case typeof value === "number":
// ✅ Narrowed to: number
console.log(value.toFixed(2));
break;
case typeof value === "boolean":
// ✅ Narrowed to: boolean
console.log(value ? "true" : "false");
break;
}
}Real-World Application: Complex Validation
interface User {
id: number;
name: string;
email?: string;
verified?: boolean;
}
function validateUser(user: Partial<User>): string {
switch (true) {
case user.id === undefined:
// ✅ TypeScript knows id might be undefined
return "ID is required";
case typeof user.id !== "number":
// ✅ Narrowed: id exists but wrong type
return "ID must be a number";
case !user.name:
// ✅ Narrowed: id is number, name missing
return "Name is required";
case user.email && !user.email.includes("@"):
// ✅ Narrowed: email exists and is string
return "Invalid email format";
default:
// ✅ Narrowed: valid user structure
return "Valid";
}
}Narrowing Comparisons for Booleans
Feature: More precise narrowing when comparing boolean variables.
Example
function processFlags(a: boolean, b: boolean) {
if (a === b) {
// ✅ Both must be same value (both true or both false)
console.log("Flags match");
} else {
// ✅ Flags differ (one true, one false)
console.log("Flags differ");
}
}
function checkFeature(enabled: boolean | undefined) {
if (enabled === true) {
// ✅ Narrowed to: true (not just boolean)
activateFeature();
} else if (enabled === false) {
// ✅ Narrowed to: false (not undefined)
deactivateFeature();
} else {
// ✅ Narrowed to: undefined
console.log("Feature not configured");
}
}instanceof Narrowing Through Symbol Access
Feature: TypeScript narrows types when accessing symbol-indexed properties after instanceof checks.
Example
class CustomError extends Error {
[Symbol.for("errorCode")]: number;
constructor(message: string, code: number) {
super(message);
this[Symbol.for("errorCode")] = code;
}
}
function handleError(error: unknown) {
if (error instanceof CustomError) {
// ✅ Narrowed to: CustomError
const code = error[Symbol.for("errorCode")];
// ✅ Type: number (preserves symbol property type)
console.log(`Error code: ${code}`);
console.log(`Message: ${error.message}`);
}
}Isolated Declarations (--isolatedDeclarations)
Performance Feature: Generate declaration files (.d.ts) without type-checking the entire program.
The Problem It Solves
Before: Declaration emit requires full type-checking, slowing down large monorepos.
// Traditional declaration emit:
// 1. Type-check entire program
// 2. Resolve all type dependencies
// 3. Generate .d.ts files
// Time: 30-60 seconds for large projects
With Isolated Declarations
Solution: Generate declarations file-by-file without cross-file type resolution.
// With --isolatedDeclarations:
// 1. Process each file independently
// 2. Generate .d.ts from explicit types
// 3. Parallel processing possible
// Time: 5-15 seconds (50-75% faster)
Requirements
Explicit Return Types Required:
// ❌ Won't work with --isolatedDeclarations
export function calculate(x: number) {
return x * 2; // Implicit return type requires inference
}
// ✅ Works with --isolatedDeclarations
export function calculate(x: number): number {
return x * 2; // Explicit return type
}
// ❌ Won't work
export const config = {
host: "localhost", // Implicit object type
port: 8080,
};
// ✅ Works
export const config: { host: string; port: number } = {
host: "localhost",
port: 8080,
};Real-World Application: Monorepo Build Performance
// tsconfig.json
{
"compilerOptions": {
"isolatedDeclarations": true,
"declaration": true,
"declarationMap": true
}
}Impact:
- 50% faster declaration emit in large monorepos
- Enables parallel .d.ts generation
- Better incremental builds
- Stricter explicit typing discipline
Breaking Changes
1. Import Attributes Syntax:
// Old assertion syntax (deprecated)
import config from "./config.json" assert { type: "json" };
// ✅ New attribute syntax (standard)
import config from "./config.json" with { type: "json" };2. Stricter --isolatedDeclarations Requirements:
Requires explicit types for exported declarations (see examples above).
Migration Guide
Step 1: Update TypeScript
npm install -D typescript@5.3Step 2: Adopt Import Attributes
Replace import assertions with attributes:
// Before
import data from "./data.json" assert { type: "json" };
// After
import data from "./data.json" with { type: "json" };Step 3: Enable Isolated Declarations (Optional)
{
"compilerOptions": {
"isolatedDeclarations": true
}
}Add explicit return types to exported functions:
// Add return types
export function processData(input: Data): ProcessedData {
// Implementation
}
// Add type annotations to exports
export const config: Config = {
// ...
};Step 4: Leverage Enhanced Narrowing
Use switch (true) for complex conditions:
// Refactor complex if-else chains
switch (true) {
case typeof value === "string":
// Handle string
break;
case typeof value === "number":
// Handle number
break;
}Performance Improvements
Declaration Emit:
- 50% faster with
--isolatedDeclarations - Parallel processing in monorepos
- Better incremental builds
Type Narrowing:
- More precise narrowing reduces unnecessary type assertions
- Better code optimization through narrower types
Editor Performance:
- Faster IntelliSense with explicit types (
--isolatedDeclarations) - Improved autocomplete in complex switch statements
Summary
TypeScript 5.3 (November 2023) brought standardized import attributes and major performance improvements:
- Import Attributes - Type-safe JSON/module imports with runtime validation
switch (true)Narrowing - Better type narrowing in switch statements- Boolean Comparison Narrowing - More precise boolean type narrowing
- Symbol Access Narrowing -
instanceofnarrowing preserves symbol properties - Isolated Declarations - 50% faster declaration emit for large projects
Impact: Import attributes standardize module metadata, while isolated declarations significantly improve build performance in monorepos.
Next Steps:
- Continue to TypeScript 5.4 for NoInfer utility type and preserved narrowing
- Return to Overview for full timeline