Einführung in ts-node

TypeScript Code direkt ausführen, ohne zuerst JavaScript Code zu erzeugen, das geht mit Tools wie ts-node. Wie man das verwenden kann und was man beachten muss, sehen wir uns anhand eines Beispiels für ein einfaches Automatisierungsscript an.

TypeScript Code direkt ausführen, ohne zuerst JavaScript Code zu erzeugen, das geht mit Tools wie ts-node. Wie man das verwenden kann und was man beachten muss, sehen wir uns anhand eines Beispiels für ein einfaches Automatisierungsscript an.

0. Wozu verwendet man ts-node?

Die Anwendungsfälle sind vielfältig. Für Automatisierungsscripte, zur Unterstützung während des Entwicklungsprozesses oder auch im produktiven Einsatz. Das Werkzeug ts-node ist sehr umfangreich und existiert schon längere Zeit. Es gibt auch neuere Tools, wie swc-node, die sich auf Performance spezialisiert haben, die dann z.B. in anderen Services verwendet werden.

Ähnlich wie bei meinen Start Templates für Node.js aus VS Code/TypeScript Web Template Update will ich herausfinden, wie man so ein Projekt mit libraries mit ts-node starten kann.

Das Ziel dabei ist, dass der Einstieg in ts-node (und swc-node) möglichst einfach ist.

1. Kompilieren von TypeScript Code und ausführen mit Node.js

Man spricht bei TypeScript meist von Transpilieren (Transpiling), weil die Kompilierung nach JavaScript erfolgt, das einen ähnlichen Abstraktionsgrad wie die Ausgangssprache hat. So ist das also im Allgemeinen bei TypeScript: Zuerst kompilieren und dann den JavaScript Code mit Node.js ausführen (oder in einer anderen JavaScript Engine, z.B. die des Browsers, ausführen).

tsc
node index.js

Die Compiler Options (tsconfig.json) sind dabei natürlich wichtig.

2. Mit ts-node ausführen

Diesen Zwischenschritt der Kompilierung ersparen wir uns mit ts-node. Es erzeugt auch keine JavaScript Files auf der Festplatte, sondern hält alles im Speicher.

ts-node index.ts

Anmerkung: Dasselbe Ergebnis erhält man auch, wenn man folgendes aufruft.

node -r ts-node/register index.ts

Ts-node nutzt also die Möglichkeit von Node.js hooks für Dateierweiterungen zu registrieren.

2.1 ts-node interaktiv

Interessant ist auch die Möglichkeit, ts-node ohne zusätzlicher Parameter zu starten, als read–eval–print loop (REPL). Die eingetippte Zeile wird bei Return ausgeführt und das Ergebnis angezeigt:

ts-node
> let a=0;
> [1, 2, 3].forEach((n) => a += n);
> a
6
> .exit

Ts-node verleiht einem das Gefühl, man kann nun TypeScript Code ausführen, genauso wie man in den F12 Tools in der Console im Browser JavaScript Code verarbeiten kann.

3. Hello World Beispiel mit ts-node

Zunächst ein sehr einfaches Beispiel, damit man sieht, was man unbedingt benötigt.

3.1 TypeScript Projekt anlegen

Ein neues Verzeichnis mit VSCode öffnen. Im Terminal (Ctrl+ö, falls es nicht angezeigt wird) dann mal ein Typescript Projekt initialisieren.

npx tsc --init

Dadurch entsteht das tsconfig.js File.

Dieses erweitern wir gleich mit der Empfehlung von ts-node (einfach den Teil ts-node vor compilerOptions einfügen.):

{
    // Most ts-node options can be specified here using their programmatic names.
    "ts-node": {
    // It is faster to skip typechecking.
    // Remove if you want ts-node to do typechecking.
    "transpileOnly": true,
    "files": true,     
    "compilerOptions": {
        // compilerOptions specified here will override those declared below,
        // but *only* in ts-node.  Useful if you want ts-node and tsc to use
        // different options with a single tsconfig.json.
    }
    },
    "compilerOptions": {
    ...

3.2 main.ts anlegen

Ein File main.ts erzeugen und mit folgendem Inhalt füllen.

Datei main.ts

console.log('Hello World!');

3.3 Libraries hinzufügen

Wir benötigen folgende Libraries:

  • typescript
  • ts-node

Anmerkung: Falls man diese Libraries bereits global installiert hat, dann ist dieser Schritt nicht nötig.

npm install --save-dev typescript ts-node

Dadurch werden nun automatisch auch package.json und package-lock.json angelegt. Die Libraries werden im Ordner node_modules gespeichert.

Das package.json sieht damit folgendermaßen aus:

{
    "devDependencies": {
    "ts-node": "^10.9.1",
    "typescript": "^4.8.4"
    }
}

3.4 Ausführen des Programms

> ts-node main.ts                          
Hello World!

Das wars. Hat man die Libraries global installiert, dann benötigt man eigentlich nur ein tsconfig.json File neben dem TapeScript File. Und selbst diese Einstellungen im tsconfig, könnte man vermutlich über Command Parameter ersetzen. Man kann auch den Ort des tsconfig Files, über Command Parameter, konfigurieren.

4. Beispiel mit ts-node

Eine typische Anwendung für ts-node wäre ein Automatisierungsscript, sodass man z.B. statt eines Powershell Scripts, TypeScript verwenden kann. Dass das Script aus nur einer Datei bestehen würde, ist mit TypeScript/JavaScript eher unrealistisch, denn man braucht dann doch gleich mal andere Bibliotheken, alleine schon für einen komfortableren Filesystem-Zugriff.

In diesem Beispiel wird ein Ordner im Filesystem angelegt, dessen Name die Kalenderwoche und des Wochentages enthält (z.B. “41-Sun” für den Sonntag in der Kalenderwoche 41). Man kann das Datum in ISO Format als Inputparameter (“2022-10-16” z.B.) angeben, wenn nicht, dann wird der aktuelle Tag verwendet.

4.1 TypeScript Projekt anlegen

Dieser Schritt ist gleich wie 3.1

Ein neues Verzeichnis mit VSCode öffnen. Im Terminal dann mal ein Typescript Projekt initialisieren.

npx tsc --init

Dadurch entsteht das tsconfig.js File.

Dieses erweitern wir gleich mit der Empfehlung von ts-node (einfach den Teil ts-node vor compilerOptions einfügen.):

{
    // Most ts-node options can be specified here using their programmatic names.
    "ts-node": {
    // It is faster to skip typechecking.
    // Remove if you want ts-node to do typechecking.
    "transpileOnly": true,
    "files": true,     
    "compilerOptions": {
        // compilerOptions specified here will override those declared below,
        // but *only* in ts-node.  Useful if you want ts-node and tsc to use
        // different options with a single tsconfig.json.
    }
    },
    "compilerOptions": {
    ...

4.2 Files anlegen

Um zu zeigen, wie externe Libraries und auch eine eigene Library dazugefügt wird, ist das Programm in 2 Files (main.ts und /common/utils.ts) aufgeteilt und verwendet date-fs und fs-extra.

Zunächst die externen libraries installieren:

npm install date-fns fs-extra

npm install --save-dev @types/fs-extra 
npm install --save-dev ts-node typescript

Dadurch wird auch das package.json file erzeugt und sieht folgendermaßen aus:

{
    "dependencies": {
    "date-fns": "^2.29.3",
    "fs-extra": "^10.1.0"
    },
    "devDependencies": {
    "@types/fs-extra": "^9.0.13",
    "ts-node": "^10.9.1",
    "typescript": "^4.8.4"
    }
}

Anmerkung: Man kann natürlich auch ts-node und typescript global installieren.

main.ts

import { Utils } from "./common/utils";

export class Main {
    public static start(inputParameter: string[]): void {

    let input = inputParameter.join(' ');
    let folderName = Utils.getCalenderWeekDirName(input);
    console.log(`folderName: ${folderName}`);

    let dirName = __dirname;
    let directoryPath = Utils.ensureDir(dirName, folderName);
    console.log(`directoryPath: ${directoryPath}`);

    }
}

const inputParameter = process.argv.slice(2);
Main.start(inputParameter);

Möglicherweise vorhandene input parameter werden an die start Methode übergeben. Die start Methode ruft zwei Methoden der Klasse Utils auf. Die eine erzeugt den Namen des neuen Ordners und die zweite legt den Ordner an. Um zu testen, ob man Module importieren kann, wurden diese Methoden in ein anderes File ausgelagert.

common/utils.ts
import { format, parseISO } from 'date-fns';
import fse from "fs-extra";
import path from "path";

export class Utils {

    public static getCalenderWeekDirName(input: string): string {
    let formattedString = "";
    try {
        const date = input ? parseISO(input) : new Date();
        formattedString = format(date, "II-EEE");
    } catch (err) {
        console.error(`ERROR in getCalenderWeekDirName(${input})!`);
        throw err;
    }
    return formattedString;
    }

    public static ensureDir(dirName: string, folderName: string): string {
    let directoryPath = "";
    try {
        directoryPath = path.join(dirName, "WEEK", folderName)
        fse.ensureDirSync(directoryPath)
    } catch (err) {
        console.error(`ERROR in ensureDir(${dirName}, ${folderName})!`);
        throw err;
    }
    return directoryPath;
    }
}

Die beiden Utils Methoden werden in diesem File implementiert. Interessant ist hier, dass zwei externe Bibliotheken verwendet werden, zum einen für die Datumsfunktionen und die fs-extra, eine Bibliothek, die fs erweitert. Sie erleichtert den Zugriff auf das Filesystem. Diese zur Verfügung gestellten Methoden sind besonders praktisch für kleine Automatisierungsaufgaben.

4.3 Testen

Ohne Input Parameter wird das aktuelle Datum verwendet:

> ts-node main.ts
folderName: 41-Sun
directoryPath: E:\Sourcecode\hlc\tsnode\tsnode-starter-2\WEEK\41-Sun

Mit Input Parameter:

> ts-node main.ts 2022-11-11
folderName: 45-Fri
directoryPath: E:\Sourcecode\hlc\tsnode\tsnode-starter-2\WEEK\45-Fri

Es wurden folgende Ordner im Verzeichnis unseres Scripts angelegt:

E:\Sourcecode\hlc\tsnode\tsnode-starter-2\WEEK
├───41-Sun
└───45-Fri

5. Debuggen in Visual Studio Code

Auch das Debuggen wird in der Dokumentation von ts-node beschrieben.

Debug Vorbereitung

Hier meine Vorgangsweise:

Debug (Ctrl+Shift+D) Dann den Link anklicken: create a launch.json file

Dann erscheint eine Auswahlliste: Select debugger: Hier Node.js auswählen.

Dadurch wird das File .vscode/launch.json erzeugt.

Dieses File wird auch automatisch geöffnet. Zum Abschnitt configurations Folgendes dazu fügen:

"runtimeArgs": [
    "-r",
    "ts-node/register"
],

Die Datei .vscode/launch.json:

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "Launch Program",
            "skipFiles": [
                "<node_internals>/**"
            ],
            "program": "${file}",
            "outFiles": [
                "${workspaceFolder}/**/*.js"
            ],
            "runtimeArgs": [
                "-r",
                "ts-node/register"
            ],
        }
    ]
}

Debuggen

Nun einen Breakpoint in einem TypeScript File setzen und mit F5 startet man den Debugger (man muss in einem TypeScript File sein, wenn man F5 klickt). In der Debug Console sieht man den Output, den man mit console.log produziert hat.

Anmerkung: Falls das Debuggen nicht mehr funktioniert, hilft manchmal ein Restart von VSCode. Falls man etwas verkonfiguriert hat, kann man auch einfach die launch.json Datei löschen und die Vorbereitungen nochmals durchführen.

6. swc-node und ts-dev-node

Während der Entwicklung kann es praktisch sein, dass es einen watch mode gibt, d.h. dass sich der Source immer wieder kompiliert, wenn man ein File ändert und speichert, dazu gibt es: ts-node-dev.

Das Projekt swc-node ist wohl ähnlich dem ts-node, übersichtlicher, neuer, aber noch schlecht dokumentiert. Es wird hier besonders auf Performance geachtet.

Um unser Beispiel von oben damit zu testen. ts-node deinstallieren und @swc-node/register installieren:

npm uninstall ts-node
npm install --save-dev @swc-node/register

Zum Starten dann folgendes aufrufen:

node -r @swc-node/register main.ts

Anmerkung: Auch ts-node kann so ähnlich aufgerufen werden:

node -r ts-node/register main.ts

7. Tipps

  • Wenn man zwischendurch mit tsc die JavaScript Files erzeugt, dann diese wieder löschen, ansonsten kann es vorkommen, dass diese von ts-node verwendet werden (Auch Unterordner ansehen)!

  • In der Readme Datei des Projektes, bz. in der Doku ist auch beschrieben, wie man es schafft, dass man dann nur mehr mit dem Filenamen das Script aufrufen kann (Shebang: #!) und nicht mehr ts-node davorstellen muss.

8. Fazit

TypeScript Code direkt ausführen, ohne den Umweg über JavaScript, das ist wohl das Ziel. Dadurch hat man nur einen Aufruf und muss nicht zuvor kompilieren.

Für kleine Automatisierungstasks, z.B. innerhalb von TypeScript Projekten (CI/CD z.B.), die man lieber in TypeScript schreiben will, weil entsprechendes Wissen vorhanden ist, ist ts-node sicher eine Erleichterung.

Die Dokumentation von ts-node ist umfangreich, aber für Einsteiger sehr verwirrend, da braucht man meiner Meinung nach tiefes TypeScript/JavaScript Wissen, um zu entscheiden welche Optionen man benötigt. Auch das Diskussions Forum fand ich eher verwirrend als hilfreich. Aber im Ernstfall sicher toll, dass es das und auch einen Discord Channel gibt.

Vielleicht habe ich auch die Anfängerdoku nur nicht gefunden, ansonsten hoffe ich, dass dieser Artikel dem einen oder anderen hilft mit ts-node zu starten.

9. Anhang

9.1 Aktuelle Versionen

❯ node --version
v16.17.0
❯ npm --version
8.15.0
❯ code --version
1.72.2

❯ ts-node --version
v10.9.1

"@swc-node/register": "1.5.4",

HLC-ts-node-twitter


Node.js - How to parse command line arguments

Artikel in diesem Blog


Weitere Let’s Code Artikel.