TypeScript Print
You're staring at your console, and it's a mess. Somewhere in that wall of [object Object] entries is the bug you're hunting, but you can't see it. You've scattered console.log() calls throughout your code, and now you're drowning in output that's either too vague or too verbose.
Sound familiar? TypeScript inherits JavaScript's console API, but knowing how to print effectively can make the difference between finding a bug in seconds versus burning an hour. In this guide, we'll cover practical techniques for printing values, objects, formatted strings, arrays, error messages, and JSON data. You'll also learn advanced console methods that most developers overlook.
Printing Values and Objects
Printing Values to the Console
For simple values, console.log() is your go-to tool. It's part of both browser and Node.js APIs, so you can use it anywhere:
console.log('Hello, World!');
const userName = 'Alice Johnson';
console.log(userName);
// Multiple values with labels
const age = 28;
console.log('Name:', userName, 'Age:', age);
You can pass multiple arguments to console.log(), and they'll be separated by spaces. This works well for quick debugging, but as your data gets more complex, you'll want better formatting options. For more advanced patterns, Convex's TypeScript best practices provide insights on effective logging strategies.
Printing Object Properties
When you're debugging objects, you have several options depending on what you need to see:
const userProfile = {
name: 'Sarah Chen',
age: 32,
role: 'senior engineer',
permissions: ['read', 'write', 'admin']
};
// Specific properties
console.log(userProfile.name); // Output: Sarah Chen
console.log(userProfile.role); // Output: senior engineer
// Entire object (browser formats it nicely)
console.log(userProfile);
// Pretty-printed JSON (best for complex nested data)
console.log(JSON.stringify(userProfile, null, 2));
/* Output:
{
"name": "Sarah Chen",
"age": 32,
"role": "senior engineer",
"permissions": [
"read",
"write",
"admin"
]
}
*/
For complex nested object structures, JSON.stringify() with indentation gives you readable output you can actually parse visually. When building real-time applications with Convex, you'll often debug complex data structures, making effective object printing crucial.
Formatting Strings and Printing Arrays
Printing Formatted Strings
Template literals are your friend when you need to embed variables in your console output:
const userName = 'Alex Martinez';
const age = 29;
const role = 'backend engineer';
// Basic template literal
console.log(`My name is ${userName} and I'm ${age} years old.`);
// Multi-line formatting for complex output
console.log(`
User Profile:
- Name: ${userName}
- Age: ${age}
- Role: ${role}
`);
// Inline calculations and method calls
const hoursWorked = 37.5;
console.log(`Weekly hours: ${hoursWorked.toFixed(1)} (${hoursWorked > 40 ? 'overtime' : 'regular'})`);
Using TypeScript string interpolation with template literals beats string concatenation every time. For complex multi-line output, check out TypeScript multiline string techniques. These patterns align well with Convex's approach to code generation where clarity is paramount.
Printing Arrays Clearly
Arrays print reasonably well by default, but for complex arrays of objects, you'll want better formatting:
const statusCodes = [200, 201, 404, 500];
const apiResponses = [
{ id: 1, status: 'success', data: { userId: 42 } },
{ id: 2, status: 'error', data: null }
];
// Simple arrays work fine as-is
console.log(statusCodes); // [200, 201, 404, 500]
// For complex arrays, use JSON.stringify
console.log(JSON.stringify(apiResponses, null, 2));
/* Output:
[
{
"id": 1,
"status": "success",
"data": {
"userId": 42
}
},
{
"id": 2,
"status": "error",
"data": null
}
]
*/
// Or loop for custom formatting
apiResponses.forEach((response, index) => {
console.log(`Response ${index + 1}: ${response.status}`);
});
Working with TypeScript array structures becomes more manageable with proper formatting. When implementing complex filtering operations as described in Convex's guide to complex filters, well-formatted output aids debugging.
Printing Error Messages and JSON Data
Printing Error Messages
When errors happen, console.error() is better than console.log() because it stands out in most consoles and indicates severity:
function processApiResponse(data: unknown) {
try {
if (!data) {
throw new Error('No data received from API');
}
// Process data...
} catch (error) {
if (error instanceof Error) {
console.error('Error:', error.message);
console.error('Stack trace:', error.stack);
} else {
console.error('Unknown error:', error);
}
}
}
// Custom error formatting
class ValidationError extends Error {
constructor(message: string, public field: string) {
super(message);
this.name = 'ValidationError';
}
}
try {
throw new ValidationError('Invalid email format', 'email');
} catch (error) {
if (error instanceof ValidationError) {
console.error(`Validation failed - Field: ${error.field}, Message: ${error.message}`);
}
}
Always use console.error() for errors rather than console.log(). It helps you (and your logging system) distinguish errors from regular debug output. Understanding the TypeScript Error type patterns helps create robust error handling. The try catch structure provides a foundation for error management, while catching the Error type ensures type safety in error handling.
Printing JSON Data
When you need to inspect API responses or complex data structures, pretty-printed JSON is invaluable:
const apiResponse = {
userId: 'user_12345',
name: 'Jordan Lee',
preferences: {
theme: 'dark',
notifications: true,
language: 'en-US'
},
tags: ['premium', 'early-adopter']
};
// Compact JSON (hard to read)
console.log(JSON.stringify(apiResponse));
// Pretty-printed JSON (much better)
console.log(JSON.stringify(apiResponse, null, 2));
// Selective serialization with replacer (only show specific keys)
console.log(JSON.stringify(apiResponse, ['userId', 'name'], 2));
// Custom serialization with replacer function
const redactSensitive = (key: string, value: any) => {
if (key === 'userId') return '[REDACTED]';
return value;
};
console.log(JSON.stringify(apiResponse, redactSensitive, 2));
The third parameter in JSON.stringify() controls indentation. Use 2 or 4 for human-readable output. Working with TypeScript JSON type definitions ensures type safety when handling JSON data. This approach aligns with Convex's TypeScript best practices for data handling in real-time applications.
Printing Variable Types for Debugging
When you're debugging unexpected behavior, checking runtime types can reveal mismatches between what you expect and what you actually have.
Printing Variable Types
The typeof operator gives you runtime type information:
const userName = 'Taylor Swift';
const age = 34;
const isVerified = true;
const accountData = { role: 'artist' };
const followCounts = [1000, 2000, 3000];
console.log(typeof userName); // Output: string
console.log(typeof age); // Output: number
console.log(typeof isVerified); // Output: boolean
console.log(typeof accountData); // Output: object
console.log(typeof followCounts); // Output: object (arrays are objects!)
// More specific type checking
console.log(Array.isArray(followCounts)); // Output: true
console.log(followCounts instanceof Array); // Output: true
// Custom type checking function
function printDetailedType(value: any, label: string = 'Value'): void {
if (Array.isArray(value)) {
console.log(`${label}: Array with ${value.length} items`);
} else if (value === null) {
console.log(`${label}: null`);
} else if (value instanceof Date) {
console.log(`${label}: Date - ${value.toISOString()}`);
} else {
console.log(`${label}: ${typeof value}`);
}
}
printDetailedType(followCounts, 'Follow counts'); // Output: Follow counts: Array with 3 items
printDetailedType(new Date(), 'Current time'); // Output: Current time: Date - 2025-12-01T...
Remember that typeof null returns 'object', which is a JavaScript quirk. Always check for null explicitly. Understanding TypeScript typeof operations helps identify runtime types during debugging. For comprehensive type system knowledge, explore TypeScript types patterns. When working with TypeScript data types, proper type checking ensures code reliability. These techniques become vital when implementing type-safe filtering patterns as described in Convex's complex filters guide.
Advanced Console Methods You Should Know
Beyond console.log(), JavaScript provides specialized console methods that can make debugging much easier.
console.table() for Structured Data
When you're dealing with arrays of objects, console.table() displays them in a readable table format:
const userActivity = [
{ userId: 101, action: 'login', timestamp: '2025-12-01T08:00:00Z' },
{ userId: 102, action: 'purchase', timestamp: '2025-12-01T08:15:00Z' },
{ userId: 101, action: 'logout', timestamp: '2025-12-01T09:30:00Z' }
];
// Instead of this...
console.log(JSON.stringify(userActivity, null, 2));
// Use this for instant readability
console.table(userActivity);
// You can also select specific columns
console.table(userActivity, ['userId', 'action']);
This works great for comparing multiple objects side-by-side. Your browser or terminal will render a nice table.
console.group() for Organizing Output
When you're logging related information, group it together to avoid clutter:
function processOrder(orderId: string, items: string[]) {
console.group(`Processing Order ${orderId}`);
console.log('Items:', items);
console.log('Total items:', items.length);
console.log('Status: Validating...');
console.groupEnd();
}
processOrder('ORD-12345', ['laptop', 'mouse', 'keyboard']);
// Use console.groupCollapsed() if you want the group collapsed by default
console.groupCollapsed('Detailed Metrics');
console.log('CPU usage: 45%');
console.log('Memory: 2.3 GB');
console.log('Active connections: 127');
console.groupEnd();
This keeps your console organized, especially when you're logging multiple operations simultaneously.
console.time() for Performance Measurement
Need to see how long an operation takes? Wrap it with timing calls:
console.time('Database Query');
// Simulate a database call
await fetch('https://api.example.com/users');
console.timeEnd('Database Query'); // Output: Database Query: 234ms
// You can have multiple timers running
console.time('Processing');
const data = processLargeDataset();
console.timeEnd('Processing');
This is much cleaner than manually tracking timestamps with Date.now().
console.trace() for Call Stacks
When you need to understand how you got to a particular point in your code, console.trace() prints the call stack:
function validateUserInput(input: string) {
if (!input) {
console.trace('Empty input detected');
throw new Error('Input cannot be empty');
}
}
function processForm(formData: { username: string }) {
validateUserInput(formData.username);
}
// When this runs, you'll see the entire call chain
processForm({ username: '' });
This is invaluable when debugging nested function calls or tracking down where a particular function is being invoked from.
CSS Styling in Console Output
You can actually style your console output with CSS using the %c directive:
console.log(
'%cSuccess!%c Operation completed',
'color: green; font-weight: bold; font-size: 16px',
'color: gray; font-size: 12px'
);
console.log(
'%cERROR',
'background: red; color: white; padding: 4px 8px; border-radius: 3px; font-weight: bold'
);
// Useful for highlighting important logs in a sea of output
console.log('%c⚠️ WARNING', 'color: orange; font-size: 14px', 'This feature is deprecated');
This is a nice trick for making critical logs stand out during development, though you'll want to remove styling before production.
When to Use Each Console Method
Different data types and debugging scenarios call for different console methods. Here's how to choose:
| Data Type | Best Method | Why |
|---|---|---|
| Simple values | console.log() | Quick and straightforward |
| Arrays of objects | console.table() | Side-by-side comparison in table format |
| Nested objects | JSON.stringify(obj, null, 2) | Full depth visibility with formatting |
| Errors | console.error() | Proper severity indication |
| Performance checks | console.time() / timeEnd() | Built-in timing without manual timestamps |
| Call stack investigation | console.trace() | See the full execution path |
| Related logs | console.group() | Keep output organized |
You'll often combine these methods. For example, use console.group() to organize a section, console.table() for the data, and console.timeEnd() to show how long it took.
Production vs Development Logging
What works during development doesn't always belong in production. Here's how to handle logging across environments:
Development: Log Freely
During development, verbose logging helps you understand what's happening:
function fetchUserData(userId: string) {
console.log('Fetching user:', userId);
console.time('User fetch');
const user = database.getUser(userId);
console.log('User data:', user);
console.timeEnd('User fetch');
return user;
}
Production: Log Strategically
In production, excessive logging hurts performance and clutters your log aggregation system. Only log what you need:
function fetchUserData(userId: string) {
try {
return database.getUser(userId);
} catch (error) {
// Only log errors in production
console.error('Failed to fetch user:', userId, error);
throw error;
}
}
Using Logging Levels
Instead of manual environment checks, use a logging library with levels:
// Simple custom logger
const logger = {
debug: (message: string, ...args: any[]) => {
if (process.env.NODE_ENV === 'development') {
console.log('[DEBUG]', message, ...args);
}
},
info: (message: string, ...args: any[]) => {
console.log('[INFO]', message, ...args);
},
warn: (message: string, ...args: any[]) => {
console.warn('[WARN]', message, ...args);
},
error: (message: string, ...args: any[]) => {
console.error('[ERROR]', message, ...args);
}
};
// Usage
logger.debug('User object:', userProfile); // Only in development
logger.error('Database connection failed'); // Always logged
For production applications, consider using libraries like winston or pino that offer log levels, formatting, and transport to external logging services.
Performance Considerations
Every console.log() has a cost. In hot code paths (functions called thousands of times per second), even logging can become a bottleneck:
// Don't do this in a tight loop
for (let i = 0; i < 1000000; i++) {
console.log('Processing item', i); // This will slow everything down
}
// Instead, log strategically
console.log('Processing 1 million items...');
for (let i = 0; i < 1000000; i++) {
// Process without logging
}
console.log('Processing complete');
Before deploying to production, search your codebase for console statements and remove debug logs that aren't providing value. Many teams use ESLint rules to warn about console usage in production code.
Final Thoughts on Printing in TypeScript
The difference between productive debugging and wasted hours often comes down to how effectively you can see what's happening in your code. Start with console.log() for quick checks, but don't stop there. Reach for console.table() when comparing objects, use console.group() to organize related output, and lean on console.time() to spot performance issues.
Here's what to remember:
- Use the right tool for the job:
console.table()for arrays of objects,JSON.stringify()for nested data,console.error()for errors - Keep production clean: Remove debug logs before deployment, or use logging levels to filter them automatically
- Style strategically: CSS styling in console output helps during development, but skip it in production
- Check types at runtime: Combine
typeof,Array.isArray(), andinstanceofto verify your assumptions
Your console is a powerful debugging tool. The better you get at using it, the faster you'll track down bugs and ship reliable TypeScript code.