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 { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
rust
use axum::{ extract::{Path, State}, response::sse::{Event, KeepAlive, Sse}, };
Streaming import progress with SSE in Axum
server-sent-events
streams
watch-channel
Advanced
7 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
typescript
import { useEffect, useState } from "react"; interface Section { id: string;
Building a scroll-spy hook in React
custom-hooks
intersectionobserver
dom-observation
Intermediate
8 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.