We converted 350 CoffeeScript files into TypeScript. This is how we did it.

The Transpilers

We are converting CoffeeScript to TypeScript with a combination of CoffeeScript v1, CoffeeScript v2, and Decaffeinate. From left to right, the transpilers/compilers become more readable and less reliable. Therefor, we are using Decaffeinate whenever possible and falling back to CoffeeScript v2 and then v1. Once the CoffeeScript has moved entirely to TypeScript, our plan is to go back over the files that did not use Decaffeinate, and given them some individual attention.

The Process

Our process has been to convert categories of files, about ten files at a time. After each batch of ten, we build, run tests, and fix problems. We have been committing to master in conversion categories; for instance, one commit is the conversion of src\deferred, the next commit is of src\effects, et cetera.

This is how it looked from the command line:

# Prepare the branch and commit.

git checkout -b convert-src-deferred-to-TypeScript
git commit -m "Convert src/deferred to TypeScript"

# Repeat the following until src/deferred is all TypeScript.

.\convert-coffee-to-type-script.ps1
npm run build    # Then open editor to fix errors. 
npm run test     # Then open editor to fix errors. 
git add -A
git commit --amend --no-edit

# Then, open up a pull request for review, if appropriate.
# After approval, merge into master.

git checkout master
git merge convert-src-deferred-to-TypeScript --no-ff

Some Logic Errors

During the process, the compiler found the following logic problems. These logic problems are human errors that the TypeScript compiler caught and that would have passed muster in a dynamic language like JavaScript.

  • Duplicate identifiers. Delete them.
  • Undefined functions. Delete the call.
  • expect(false === true). Fix the test.

Some Conversion Errors

It also found the following conversion problems. These conversion problems are due to the transpilation not due to human error.

When we imported them with require, objects exported with export default had a top level default property.

Other modules had this...

// error TS1192: Module '"chai"' has no default export.
import chai from 'chai';

// solution
import * as chai from 'chai';

The PowerShell Script

This is the convert-coffee-to-type-script.ps1 PowerShell script that we used.

function likeAny($str, $patterns) {
    foreach ($pattern in $patterns) { 
        $p = "*" + $pattern + "*";
        if ($str -like $p) { return $true; } 
    }
    return $false;
}

# make sure we're working with a clean working tree
git clean -xfd -e "node_modules"

# uncomment directories that we're ready to process
$directoriesToProcess = @(
    "___placeholder___"
    #, "test\unit"
    #, "test\integration"
    #, "src\queue"
    #, "src\effects"
    , "src\deferred"
    , "et\cetera"
    , "et\cetera"
    , "et\cetera"
);

# get target CoffeeScript files to process
$coffeeScriptFiles = Get-ChildItem -Filter *.coffee -Recurse | 
    Where-Object { $_.FullName -notlike "*node_modules*" } | 
    Where-Object { $_.FullName -notlike "*public\docs*" } | 
    Where-Object { likeAny $_.FullName $directoriesToProcess } |
    Select-Object -ExpandProperty FullName -Last 10

# convert to JavaScript
# using either CoffeeScript or Decaffeinate
$coffeeScriptFiles | ForEach-Object {
    # .\node_modules\.bin\coffee --compile $_
    decaffeinate $_
    Write-Output $_
    Remove-Item $_
}

# get the resultant JavaScript files
$javaScriptFiles = $coffeeScriptFiles | 
    ForEach-Object { [IO.Path]::ChangeExtension($_, "js") }

# prepend comments to top of the file
$javaScriptFiles | 
    ForEach-Object {
        @("/* A script converted this file to TypeScript. */") + 
        @("/* tslint:disable */") + 
        (Get-Content $_) | set-content $_
    }

# rename to TypeScript
$javaScriptFiles | 
    Rename-Item -NewName { [IO.Path]::ChangeExtension($_, "ts") }