Making a CSV file reader using TypeScript

·

5 min read

Assuming you have concepts of classes and how we declare classes in TypeScript, we will look into how we are going to write a CSV filer reader class in this blog.

Also, if you are not familiar with what a CSV file is, here is a definition I looked up on the internet for you

A CSV is a comma-separated values file, which allows data to be saved in a tabular format. CSVs look like a garden-variety spreadsheet but with a .csv extension.

CSV files can be used with most any spreadsheet program, such as Microsoft Excel or Google Spreadsheets.

Let's see the initial declaration of our CSV class


export class CsvFileReader{
    data: string[][] = [][];
    constructor(public filename : string){
    }
}

We are writing public before the filename in the constructor because we will make a parameter in our class that will be public every time a 'filename' is passed in the constructor and it's value will be equal to the argument passed when we make an instance of the class.

Now since we declared basic structure of the CsvFileReader class let's see how we are going to declare methods

Declaring methods in the class

We will declare a method 'read' so we will process the information of the csv file being passed. For that we will use the 'fs' module provided by node. fs stands for File System

import fs from 'fs';

export class CsvFileReader{
    data : string[][] = [][];

    constructor(public fileName : string){
    }

    read() : void {
  X      this.data = fs.readFileSync(`${this.fileName}`,{
                   encoding : 'utf-8'
        }).split('\n').map((row : string) : string[] => {
                return row.split(',')
        })
    }
}

The entire function of .split().map() is used to split the data from csv file. It is pretty much standard. Now we will have to handle the data types

Handling Data types

For this example, we have to remember this and our operations will be based on this basis only

We will have a string, a number, a date being parsed from the csv file we need and parse them all to their respective data types

Converting dateString to Date : dateStringToDate functiom

For convenience create a function that will parse the data string '28/10/2018' to [28,10,2018]. We will see why we want it like this


export const dateStringToDate = (dateString : string) : Date => {

    // dateString = 28/10/2018
    const dateParts = dateString.split('/')
                    .map( (value : string) : number => {
                          return Number.parseInt(value)
                    })
    // dateParts = [28,10,2018]
    return new Date(dateParts[2], dateParts[1] - 1, dateParts[0]);
}

See how we are using dateParts[1] -1 in the second argument of the Date class. If we won't subtract the 1, we will get an incremented value in the month ie. instead of 10, we will get month as 11.

Declaring Tuple for the CSV data : MatchData

In our CSV file we will have the Matchdata type in the following order

  1. Date

  2. string

  3. string

  4. number

  5. number

  6. MatchResult

  7. String

So we declare a separate type for our Match data through tuple. Declaring tuple requires starting with the type followed by tuple name.

type MatchData = [
    Date,
    string,
    string,
    number,
    number,
    MatchResult, // we will declare this type separately
    string
]

We will now use type assertions to override the TS behavior

In our read() method we are returning a string[], now we will take this string[] and return it as MatchData type. This is the implementation we will do


map((row : string[]) : MatchData => {
    return [
            dateStringToDate(row[0]),
            row[1],row[2],
            Number.parseInt(row[3]),
            Number.parseInt(row[4]),
            row[5] as MatchResult, // line X
            row[6]
           ]
    })

MatchResult tuple

In line X we are using type assertion. MatchResult is an enum that we have declared.

This is how will implement the MatchResult


enum MatchResult {
  WON = "WON",
  LOST = "LOST",
  DRAW = "DRAW",
}

Making the class independent : CsvFileReader class

We have an entire class which depends on the data of the MatchData tuple to return a value.

We need to have an independent class that reads the CSV file and return us a string no matter the order of the data in the CSV file

These are steps we need to take

  1. Make the CsvFileReader an abstract class

  2. Use the CsvFileReader class( no need to create an instance since it is now an abstract class), make it's return type on the basis of the matchData tuple that we will provide it from outside.

import fs from 'fs';
import {MatchData} from './MatchDataTupleCreated' // you can name it anything. This is just the tuple of the data order of the CSV file

export abstract class CsvFileReader{
    data : MatchData[] = [];
    filename : string;
    constructor(filename : string){
        this.filename = filename;
    }

    abstract mapRow(row : string[]) : MatchData

    read() : void {
    this.data = fs.readFileSync(`${this.filename}`,{
                encoding : 'utf-8'
            }).split('\n').
            .map((row : string) : string[] => {
                return row.split(',')
            })
            .map(this.mapRow)
    }
}

mapRow is an abstract method and hence it does not need a body. We know how we want it's implementation as we have seen in the section 'Declaring Tuple for the CSV data'.

For now we will focus on renaming the MatchData to a generic type T for ease of writing the code

import fs from 'fs';

export abstract class CsvFileReader<T>{
    data : T[] = [];
    filename : string ;

    constructor(filename : string){    
        this.filename = filename;
    }

    abstract mapRow(row : string[]) : T // abstract method so not implementing the body

    read() : void {
        this.data = fs.readFileSync(`${this.filename}`,{
                encoding : 'utf-8'
            }).split('\n').
            .map((row : string) : string[] => {
                return row.split(',')
            })
            .map(this.mapRow)
    }
}

Putting everything together

Now we have the following implementations

  1. An abstract class CsvFileReader

  2. A MatchData tuple

  3. A dateStringToDate converting function

  4. MatchResult enum

Using all these we will now implement a MatchReader class


import {CsvFileReader} from '../path';
import {MatchData} from '../path2';

import {dateStringToDate} from '../path3';
import {MatchResult} from '../path4';

export class MatchReader extends CsvFileReader<MatchData>{
    mapRow(row: string[]) : MatchData {
            return [
                    dateStringToDate(row[0]),
                    row[1],row[2],
                    Number.parseInt(row[3]),
                    Number.parseInt(row[4]),
                    row[5] as MatchResult, 
                    row[6]
                   ]    
    })
}

This is how we will do the custom CSV class using inheritance in TypeScript