16 Control Structures in TypeScript for VSCode Extensions

16.1 Technical Overview

Control structures govern the program flow in extensions and are essential for implementing commands, event handlers, and API interactions. TypeScript extends JavaScript control structures with type safety and offers specific advantages over Java for extension developers: automatic Promise handling with async/await, optional chaining for safe object access, and union types for precise switch statements.

The VSCode API works heavily with asynchronous operations, conditional configuration access, and error handling. This chapter demonstrates the practical use of the most important control structures in extension contexts.

16.2 Conditional Execution with if/else

Extensions often need to react to environmental conditions: Is an editor active? Does a file exist? Does the user have specific rights? TypeScript’s if/else syntax is identical to Java but offers enhanced capabilities through optional chaining and type inference.

// Typical extension checks using modern TypeScript features
function saveCurrentFile(): void {
    const editor = vscode.window.activeTextEditor;
    
    // Optional chaining avoids null-pointer-like errors
    if (editor?.document.isDirty) {
        editor.document.save();
        vscode.window.showInformationMessage('File saved');
    } else if (!editor) {
        vscode.window.showWarningMessage('No active editor');
    } else {
        vscode.window.showInformationMessage('File already saved');
    }
}

The ?. operator checks for null, undefined, and property existence simultaneously. Compared to Java’s verbose null-checking, this significantly reduces code redundancy.

Conditional command availability illustrates a typical extension use case:

function registerConditionalCommands(context: vscode.ExtensionContext): void {
    // Register command only if a workspace is open
    if (vscode.workspace.workspaceFolders) {
        const workspaceCommand = vscode.commands.registerCommand('demo.workspaceInfo', () => {
            const folderCount = vscode.workspace.workspaceFolders!.length;
            vscode.window.showInformationMessage(`Workspace has ${folderCount} folders`);
        });
        context.subscriptions.push(workspaceCommand);
    }
}

The exclamation mark (!) after the array access is TypeScript’s non-null assertion – safely usable here since the if-condition guarantees existence.

16.3 Switch Statements for API Decisions

Switch statements are ideal for command routing and configuration evaluation. TypeScript’s union types make switch statements type-safe and allow the compiler to check for completeness.

// Union type for defined command categories
type CommandCategory = 'file' | 'edit' | 'debug' | 'view';

function executeCommandByCategory(category: CommandCategory, action: string): void {
    switch (category) {
        case 'file':
            if (action === 'save') {
                vscode.commands.executeCommand('workbench.action.files.save');
            } else if (action === 'open') {
                vscode.commands.executeCommand('workbench.action.files.openFile');
            }
            break;
            
        case 'edit':
            vscode.commands.executeCommand(`editor.action.${action}`);
            break;
            
        case 'debug':
            vscode.commands.executeCommand(`workbench.action.debug.${action}`);
            break;
            
        case 'view':
            vscode.commands.executeCommand(`workbench.action.${action}`);
            break;
            
        // TypeScript detects missing cases
        default:
            const exhaustiveCheck: never = category;
            throw new Error(`Unknown category: ${exhaustiveCheck}`);
    }
}

The never type in the default case is a TypeScript specialty: if all union type values have been handled, category is of type never. If a case is missing, the compiler throws a type error.

Configuration evaluation shows another practical application:

function applyLogLevel(): void {
    const config = vscode.workspace.getConfiguration('demo');
    const logLevel = config.get<string>('logLevel', 'info');
    
    switch (logLevel) {
        case 'verbose':
            console.log('Verbose logging enabled');
            // Fall-through intended
        case 'info':
            console.log('Info logging enabled');
            break;
        case 'warn':
            console.warn('Only warnings and errors logged');
            break;
        case 'error':
            console.error('Only errors logged');
            break;
        default:
            console.log('Unknown log level, using info');
    }
}

16.4 Asynchronous Control with async/await

VSCode extensions primarily work asynchronously. TypeScript’s async/await simplifies Promise handling compared to Java’s CompletableFuture syntax and enables sequential readability for asynchronous operations.

// Sequential file operations with error handling
async function processMultipleFiles(): Promise<void> {
    const files = await vscode.workspace.findFiles('**/*.ts', '**/node_modules/**');
    
    for (const file of files) {
        try {
            const document = await vscode.workspace.openTextDocument(file);
            const content = document.getText();
            
            // Asynchronous processing with await
            if (content.includes('TODO')) {
                await vscode.window.showInformationMessage(`TODO found in ${file.fsPath}`);
            }
        } catch (error) {
            console.error(`Failed to process ${file.fsPath}:`, error);
        }
    }
}

The for...of loop with await is particularly important: Unlike forEach or map, it executes operations sequentially. Parallel processing would require Promise.all().

Asynchronous user interaction illustrates practical use:

async function promptUserAction(): Promise<void> {
    const selection = await vscode.window.showQuickPick(
        ['Save All', 'Close All', 'Cancel'],
        { placeHolder: 'Choose action' }
    );
    
    // Conditional execution based on user selection
    if (selection === 'Save All') {
        await vscode.workspace.saveAll();
        vscode.window.showInformationMessage('All files saved');
    } else if (selection === 'Close All') {
        await vscode.commands.executeCommand('workbench.action.closeAllEditors');
    }
    // On Cancel or Escape, selection is undefined – no action needed
}

16.5 Error Handling with try/catch

Extension code must robustly handle API errors, file system issues, and user input. TypeScript’s try/catch offers typed error handling and enhanced capabilities through finally blocks.

async function safeFileOperation(fileName: string): Promise<boolean> {
    let operationSuccess = false;
    
    try {
        // Multiple potentially failing operations
        const uri = vscode.Uri.file(fileName);
        const stat = await vscode.workspace.fs.stat(uri);
        
        if (stat.type === vscode.FileType.File) {
            const content = await vscode.workspace.fs.readFile(uri);
            console.log(`File size: ${content.length} bytes`);
            operationSuccess = true;
        }
        
    } catch (error) {
        // TypeScript-specific error handling
        if (error instanceof vscode.FileSystemError) {
            vscode.window.showErrorMessage(`File system error: ${error.message}`);
        } else if (error instanceof Error) {
            vscode.window.showErrorMessage(`General error: ${error.message}`);
        } else {
            vscode.window.showErrorMessage('Unknown error occurred');
        }
        
    } finally {
        // Cleanup or logging, always executed
        console.log(`File operation completed: ${operationSuccess}`);
    }
    
    return operationSuccess;
}

The instanceof checks are TypeScript type guards – they narrow the type of error within the respective if blocks.

16.6 Iteration and Loops

Extensions frequently process file lists, editor contents, or configuration arrays. TypeScript offers modern iteration methods that differ significantly from Java’s iterator pattern.

function analyzeWorkspaceFiles(): void {
    const editors = vscode.window.visibleTextEditors;
    
    // for...of for arrays with objects
    for (const editor of editors) {
        const { document } = editor;
        console.log(`Analyzing: ${document.fileName}`);
        
        // for...of for string iteration (line by line)
        const lines = document.getText().split('\n');
        for (const [index, line] of lines.entries()) {
            if (line.trim().startsWith('//')) {
                console.log(`Comment at line ${index + 1}: ${line.trim()}`);
            }
        }
    }
    
    // Classic for loop for index-based access
    for (let i = 0; i < editors.length; i++) {
        const position = new vscode.Position(0, 0);
        editors[i].selection = new vscode.Selection(position, position);
    }
}

The entries() method is particularly useful: it provides both index and value; destructuring makes its use elegant.

Conditional iteration with while appears in event-driven operations:

async function waitForDocumentReady(document: vscode.TextDocument): Promise<void> {
    let attempts = 0;
    const maxAttempts = 10;
    
    while (document.isDirty && attempts < maxAttempts) {
        console.log(`Waiting for document to be saved... Attempt ${attempts + 1}`);
        await new Promise(resolve => setTimeout(resolve, 100)); // wait 100ms
        attempts++;
    }
    
    if (attempts >= maxAttempts) {
        throw new Error('Document not ready after maximum attempts');
    }
}

16.7 Integration into Extension Architecture

The control structures presented work together in typical extension scenarios. A command handler demonstrates the practical combination:

async function handleComplexCommand(): Promise<void> {
    try {
        // Conditional pre-checks
        if (!vscode.window.activeTextEditor) {
            vscode.window.showWarningMessage('No active editor');
            return;
        }
        
        const editor = vscode.window.activeTextEditor;
        const config = vscode.workspace.getConfiguration('demo');
        const mode = config.get<string>('processingMode', 'safe');
        
        // Switch for processing mode
        switch (mode) {
            case 'fast':
                await processFast(editor);
                break;
            case 'thorough':
                await processThorough(editor);
                break;
            default:
                await processSafe(editor);
        }
        
        vscode.window.showInformationMessage('Processing completed');
        
    } catch (error) {
        const message = error instanceof Error ? error.message : 'Unknown error';
        vscode.window.showErrorMessage(`Processing failed: ${message}`);
    }
}

async function processSafe(editor: vscode.TextEditor): Promise<void> {
    const document = editor.document;
    
    // Iteration through all lines with error handling
    for (let i = 0; i < document.lineCount; i++) {
        try {
            const line = document.lineAt(i);
            if (line.text.trim()) { // Only non-empty lines
                console.log(`Processing line ${i + 1}: ${line.text.substring(0, 50)}...`);
            }
        } catch (lineError) {
            console.warn(`Skipped line ${i + 1} due to error:`, lineError);
            continue; // Continue with next line
        }
    }
}

This structure shows the interplay of all covered control structures in a realistic extension scenario: conditional checks, switch-based mode selection, try/catch error handling, and safe iteration.