typescript
38 lines · 7 steps
Building a type-safe CSV writer in TypeScript
A generic class turns rows of any shape into correctly escaped CSV using per-column value extractors.
Explained by
highlit
1type CsvColumn<T> = {
2 header: string;
3 value: (row: T) => string | number | boolean | null | undefined;
4};
5
6export class CsvWriter<T> {
7 constructor(
8 private readonly columns: CsvColumn<T>[],
9 private readonly delimiter = ",",
10 ) {}
11
12 toString(rows: Iterable<T>): string {
13 const lines = [this.formatRow(this.columns.map((c) => c.header))];
14 for (const row of rows) {
15 lines.push(this.formatRow(this.columns.map((c) => this.render(c.value(row)))));
16 }
17 return lines.join("\r\n");
18 }
19
20 private render(value: string | number | boolean | null | undefined): string {
21 if (value === null || value === undefined) return "";
22 return String(value);
23 }
24
25 private formatRow(fields: string[]): string {
26 return fields.map((field) => this.escape(field)).join(this.delimiter);
27 }
28
29 private escape(field: string): string {
30 const mustQuote =
31 field.includes(this.delimiter) ||
32 field.includes("\"") ||
33 field.includes("\n") ||
34 field.includes("\r");
35 if (!mustQuote) return field;
36 return `"${field.replace(/"/g, '""')}"`;
37 }
38}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Defining columns as extractor functions decouples the CSV structure from the shape of your data.
- 2Correct CSV output hinges on quoting fields that contain delimiters, quotes, or newlines and doubling embedded quotes.
- 3Generics let one writer serialize any row type while keeping value extraction fully type-checked.
Related explainers
typescript
import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { ValidationPipe } from '@nestjs/common'; import { ApiProperty } from '@nestjs/swagger';
Wiring validation and Swagger docs in NestJS
validation
openapi
decorators
Intermediate
8 steps
typescript
import { Component, Input } from '@angular/core'; interface Order { id: string;
How Angular ICU plurals localize an order summary
i18n
pluralization
standalone-component
Intermediate
8 steps
ruby
class KeyTransformer def self.camelize(data) new.camelize(data) end
Recursively camelizing nested Ruby data
recursion
data-transformation
pattern-matching
Intermediate
7 steps
typescript
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable, catchError, concatMap, finalize } from 'rxjs'; import { DataSource, QueryRunner } from 'typeorm';
Wrapping requests in a transaction with NestJS
interceptors
transactions
rxjs
Advanced
7 steps
typescript
import { parsePhoneNumberFromString, CountryCode } from 'libphonenumber-js'; export interface NormalizedPhone { e164: string;
Normalizing phone numbers to E.164 in TypeScript
validation
normalization
error-handling
Intermediate
7 steps
typescript
import { CanActivate, ExecutionContext, Injectable,
Role-based access with a NestJS guard
authorization
guards
decorators
Intermediate
6 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-type-safe-csv-writer-in-typescript-explained-typescript-8e00/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.