Site WWW de Laurent Bloch
Slogan du site

ISSN 2271-3905
Cliquez ici si vous voulez visiter mon autre site, orienté vers des sujets moins techniques.

Pour recevoir (au plus une fois par semaine) les nouveautés de ce site, indiquez ici votre adresse électronique :

Programmer en TypeScript avec ChatGPT, second épisode
Article mis en ligne le 1er octobre 2025
dernière modification le 2 octobre 2025

par Laurent Bloch

Implorer le pardon des gens vraiment compétents

Dans un article précédent je racontais mes aventures de débutant avec le langage de programmation TypeScript, et comment ChatGPT m’avait aidé (je suis loin d’être le seul). Bon, il ne manque pas de gens sur le réseau pour expliquer que cette démarche est mauvaise, et j’ai moi-même rendu compte d’une étude très sérieuse qui explique qu’elle ne saurait que faire perdre du temps à un programmeur expérimenté et expert de son domaine. Je ne parle ici que pour les débutants, comme moi en l’occurrence avec TypeScript.

Pour aborder un nouveau langage de programmation, il faut en apprendre la syntaxe, et cela peut être pénible. Or la syntaxe ne présente aucun intérêt, et justement, au contraire des langages humains, la syntaxe des langages de programmation est rigide et univoque : cela convient bien à un système d’IA. Il faut aussi découvrir les noms de toutes les fonctions des bibliothèques de calcul, d’entrée-sortie, de manipulation de chaînes de caractères et de structures de données diverses et variées, et les créateurs de langages sont dotés pour cela d’une imagination débordante : un débutant humain pourrait passer des heures à fouiller la documentation, mais un agent IA fait cela bien plus vite. On aurait tort de se priver de son aide.

Justement pour ce second épisode je me suis attaqué aux entrées-sorties, un domaine dépourvu de noblesse intellectuelle, mais néanmoins plein de complication, puisqu’il faut établir des communications entre le programme et des objets matériels, par l’intermédiaire du système d’exploitation, des objets d’une grande complexité, et là aussi ChatGPT m’a aidé, sans que pour autant je cesse de maîtriser mon code, parce que cela reste concis et intelligible.

Alors allons-y.

Lire des données dans un fichier

Pour continuer la lignée de mes langages précédents, je lirai des extraits de la banque de données de protéines SwissProt, un fichier texte séquentiel, et j’en extrairai le texte de la séquence proprement dite, débarrassée des informations descriptives destinées au lecteur humain, pour la réduire au format dit FASTA, accepté par tous les logiciels d’analyse.

Voici une entrée de la banque SwissProt ; elle donne la séquence du gène YWHAB chez le Xénope [1], une grande grenouille africaine griffue. Les deux premières colonnes de chaque ligne identifie un type de ligne. Les lignes qui nous intéresseront sont la ligne "AC", qui donne le ou les Accession Numbers qui identifient la séquence de façon unique par une nomenclature internationale, et, à la fin, les lignes dont les deux premières colonnes sont vides, et qui donnent la séquence d’acides aminés de la protéine, codés selon le code génétique :

ID   1433B_XENTR             Reviewed;         244 AA.
AC   Q5XGC8; Q28HK2;
DT   22-NOV-2005, integrated into UniProtKB/Swiss-Prot.
DT   23-NOV-2004, sequence version 1.
DT   28-NOV-2006, entry version 18.
DE   14-3-3 protein beta/alpha.
GN   Name=ywhab;
OS   Xenopus tropicalis (Western clawed frog) (Silurana tropicalis).
OC   Eukaryota; Metazoa; Chordata; Craniata; Vertebrata; Euteleostomi;
OC   Amphibia; Batrachia; Anura; Mesobatrachia; Pipoidea; Pipidae;
OC   Xenopodinae; Xenopus; Silurana.
OX   NCBI_TaxID=8364;
RN   [1]
RP   NUCLEOTIDE SEQUENCE [LARGE SCALE MRNA].
RG   Sanger Xenopus tropicalis EST/cDNA project;
RL   Submitted (MAR-2006) to the EMBL/GenBank/DDBJ databases.
RN   [2]
RP   NUCLEOTIDE SEQUENCE [LARGE SCALE MRNA].
RC   TISSUE=Embryo;
RG   NIH - Xenopus Gene Collection (XGC) project;
RL   Submitted (OCT-2004) to the EMBL/GenBank/DDBJ databases.
CC   -!- FUNCTION: Adapter protein implicated in the regulation of a large
CC       spectrum of both general and specialized signaling pathway. Binds
CC       to a large number of partners, usually by recognition of a
CC       phosphoserine or phosphothreonine motif. Binding generally results
CC       in the modulation of the activity of the binding partner (By
CC       similarity).
CC   -!- SUBUNIT: Homodimer (By similarity).
CC   -!- SUBCELLULAR LOCATION: Cytoplasm (By similarity).
CC   -!- SIMILARITY: Belongs to the 14-3-3 family.
CC   -----------------------------------------------------------------------
CC   Copyrighted by the UniProt Consortium, see http://www.uniprot.org/terms
CC   Distributed under the Creative Commons Attribution-NoDerivs License
CC   -----------------------------------------------------------------------
DR   EMBL; CR760847; CAJ82973.1; -; mRNA.
DR   EMBL; BC084514; AAH84514.1; -; mRNA.
DR   UniGene; Str.8742; -.
DR   SMR; Q5XGC8; 1-231.
DR   Ensembl; ENSXETG00000022830; Xenopus tropicalis.
DR   InterPro; IPR000308; 14-3-3.
DR   Gene3D; G3DSA:1.20.190.20; 14-3-3; 1.
DR   PANTHER; PTHR18860; 14-3-3; 1.
DR   Pfam; PF00244; 14-3-3; 1.
DR   PRINTS; PR00305; 1433ZETA.
DR   ProDom; PD000600; 14-3-3; 1.
DR   SMART; SM00101; 14_3_3; 1.
DR   PROSITE; PS00796; 1433_1; 1.
DR   PROSITE; PS00797; 1433_2; 1.
KW   Acetylation.
FT   CHAIN         1    244       14-3-3 protein beta/alpha.
FT                                /FTId=PRO_0000058600.
FT   MOD_RES       1      1       N-acetylmethionine (By similarity).
SQ   SEQUENCE   244 AA;  27721 MW;  FF766793EA1CA9E5 CRC64;
     MDKSELVQKA KLSEQAERYD DMAASMKAVT ELGAELSNEE RNLLSVAYKN VVGARRSSWR
     VISSIEQKTE GNDKRQQMAR EYREKVETEL QDICKDVLGL LDKYLVPNAT PPESKVFYLK
     MKGDYYRYLS EVASGDSKQE TVTCSQQAYQ EAFEISKSEM QPTHPIRLGL ALNFSVFYYE
     ILNSPEKACS LAKSAFDEAI AELDTLNEES YKDSTLIMQL LRDNLTLWTS ENQGEEADNA
     EADN
//

Et voici le fichier FASTA que je veux en extraire, après suppression des espaces et des commentaires :

>Q5XGC8;Q28HK2;
MDKSELVQKAKLSEQAERYDDMAASMKAVTELGAELSNEERNLLSVAYKNVVGARRSSWR
VISSIEQKTEGNDKRQQMAREYREKVETELQDICKDVLGLLDKYLVPNATPPESKVFYLK
MKGDYYRYLSEVASGDSKQETVTCSQQAYQEAFEISKSEMQPTHPIRLGLALNFSVFYYE
ILNSPEKACSLAKSAFDEAIAELDTLNEESYKDSTLIMQLLRDNLTLWTSENQGEEADNA
EADN

Premier dialogue avec ChatGPT

En TypeScript, pour NodeJs, comment déclare-t-on un fichier ? Quelle est la méthode pour l’ouvrir ? lire une ligne ?

En Node.js avec TypeScript, tu travailles avec les modules intégrés de Node, notamment fs (file system).

Contrairement à certains langages (C, Pascal, etc.), on ne "déclare" pas un fichier avec un mot-clé particulier : on importe simplement le module fs et on manipule les fichiers via ses fonctions.

1. Déclarer un fichier (ouvrir/écrire) :

En TypeScript, tu importes fs ainsi :

import * as fs from "fs";

Pour écrire dans un fichier :

fs.writeFileSync("monFichier.txt", "Bonjour, monde !");

2. Lire un fichier ligne par ligne :

En Node, on utilise le module readline avec un ReadableStream :

import * as fs from "fs";
import * as readline from "readline";

async function lireLignes(fichier: string) {
    const rl = readline.createInterface({
        input: fs.createReadStream(fichier),
        crlfDelay: Infinity, // gère correctement \n et \r\n
    });

    for await (const ligne of rl) {
        console.log("Ligne lue :", ligne);
    }
}

lireLignes("monFichier.txt");

✅ Résumé :

Pas de "déclaration" spéciale, juste import * as fs.

fs.openSync ou fs.open pour ouvrir.

fs.readFileSync / fs.readFile pour lire en bloc.

readline + fs.createReadStream pour lire ligne par ligne.

Abandonner la distribution Ubuntu des paquets NodeJS et Npm

Pour réparer NodeJS et Npm (packages Ubuntu pas terribles) :

$ sudo apt purge npm
$ sudo apt purge nodejs
$ sudo apt autoremove -y

Installe Node via NodeSource ou nvm.

Option 1 : avec nvm (recommandé)

# installer nvm si pas déjà présent
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

# recharger ton shell pour que nvm soit disponible
source ~/.bashrc

# Installer la dernière version LTS (20.x à ce jour)
nvm install 20

# Utiliser cette version
nvm use 20

# Vérifier npm
npm -v

# Nettoyer ton projet
# Dans ton projet
rm -rf node_modules package-lock.json

# Installer TypeScript et les types Node

npm install --save-dev typescript @types/node

# Créer un tsconfig.json minimal pour Node

# Dans ton projet, crée tsconfig.json :

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "moduleResolution": "node",
    "types": ["node"],
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"]
}

# Avec cette méthode, tu as un Node.js moderne, stable, et indépendant du paquet Ubuntu.
# Plus de internal/deps/cjs-module-lexer qui plante npm

L’organisation du code a changé : leçons modernes

La façon d’organiser un projet informatique a pas mal changé depuis mes débuts au siècle dernier. Jadis, on installait un compilateur dans l’arborescence du système, et tous les programmes à construire l’utilisaient de la même façon. Aujourd’hui, on crée un répertoire par projet, et c’est à partir de ce répertoire que l’on effectue les opérations d’installation énumérées ci-dessus, parce que la configuration de la chaîne de compilation peut être plus compliquée qu’un unique compilateur, et qu’elle peut varier d’un projet à un autre.

Puis on place les fichiers sources dans un sous-répertoire src, ce n’est pas obligatoire mais usuel, c’est écrit dans le fichier de configuration tsconfig.json. Je place les fichiers de données dans un sous-répertoire Data.

C’est ce que nous avons vu ci-dessus, avec les conseils judicieux de ChatGPT.

Lire le fichier de séquence et le filtrer pour obtenir un fichier Fasta

Nous allons ajouter au programme ci-dessus une fonction filtrerFasta pour omettre les lignes de commentaires à usage humain, ajouter la ligne de commentaire du format Fasta avec les données de la ligne AC, retirer les espaces blancs des lignes de séquence. Par la même occasion nous allons lui ajouter de quoi l’invoquer depuis la ligne de commande en important la classe Command :

#!/usr/bin/env -S npx tsx
import { Command } from "@commander-js/extra-typings";

// Lire une séquence
import * as fs from 'fs';
import * as readline from 'readline';

async function lireLignes(fichier: string) {
    const rl = readline.createInterface({
        input: fs.createReadStream(fichier),
        crlfDelay: Infinity, // gère correctement \n et \r\n
    });

    for await (const ligne of rl) { 
        filtrerFasta(ligne);
      }
}

function filtrerFasta(ligne: string) { 
    if (ligne.substring(0, 2) === "AC") {
        console.log(">" + ligne.substr(2).replace(/\s/g, ""));
    }
    else if (ligne.substring(0, 2) === "  ") {
        console.log(ligne.replace(/\s/g, ""));
    }
}

lireLignes("une-sequence.txt");

On observe que l’argument de la méthode replace(/\s/g, "") contient une expression régulière :

 /\s/ indique le motif qui sera la cible de la méthode replace, le motif est encadré par des barres obliques /, il est noté \s, ce qui désigne un espace ou caractère équivalent, tel que tabulation ou espace insécable ;
 le caractère g indique que le remplacement sera global, c’est-à-dire que si le motif apparaît plusieurs fois dans la même ligne de texte, il sera remplacée à chaque fois ;
 la chaîne "" indique ce qui remplacera le motif cible, en l’occurrence la chaîne vide.

Passer le nom du fichier en argument de la ligne de commande

Coder « en dur » dans le programme le nom du fichier à traiter est singulièrement malcommode. Voici comment passer le nom de fichier en argument à l’appel du programme depuis la ligne de commande ; la classe Command permet d’instancier notre programme program en commande du shell, capable de recevoir des arguments et des options :

#!/usr/bin/env -S npx tsx
import { Command } from "@commander-js/extra-typings";

// Lire une séquence
import * as fs from "fs";      
import * as readline from "readline"; 

// --- Programme principal avec Commander ---
const program = new Command();

program
    .name("lire-fasta")
    .description("Lire un fichier séquentiel SwissProt et imprimer la séquence Fasta")
    .argument("<fichier>", "Nom du fichier à lire") 
    .action(async (fichier: string) => {
	await lireLignes(fichier);
  });

program.parse(); // ← déclenche le parsing des arguments

// --- Fonctions utilitaires ---
async function lireLignes(fichier: string) {
    const rl = readline.createInterface({
        input: fs.createReadStream(fichier),
        crlfDelay: Infinity, // gère correctement \n et \r\n
    });

    for await (const ligne of rl) { 
        filtrerFasta(ligne);
      }
}

function filtrerFasta(ligne: string) { 
    if (ligne.substring(0, 2) === "AC") {
        console.log(">" + ligne.substr(2).replace(/\s/g, ""));
    }
    else if (ligne.substring(0, 2) === "  ") {
        console.log(ligne.replace(/\s/g, ""));
    }
}

On observera, au début du programme, la série de déclarations de méthodes qui suivent l’invocation de program.

La méthode .parse déclenche l’analyse des arguments et donc l’exécution du programme.

Lire plusieurs fichiers à la suite

Nous aimerions aussi que notre programme puisse lire des séquences dans plusieurs fichiers, ce qui suppose qu’il puisse itérer sur une liste de noms de fichiers, ainsi :

#!/usr/bin/env -S npx tsx
import { Command } from "commander";
import * as fs from "fs";
import * as readline from "readline";

// --- Programme principal avec Commander ---
const program = new Command();

program
  .name("lire-fasta")
  .description("Lire un ou plusieurs fichiers séquentiels SwissProt et imprimer les séquences Fasta")
  .argument("<fichiers...>", "Un ou plusieurs fichiers à lire")
  .action(async (fichiers: string[]) => {
    for (const fichier of fichiers) { 
      console.error(`\n--- Lecture de ${fichier} ---`);
      await lireLignes(fichier);
    }
  });

program.parse();

// --- Fonctions utilitaires ---
async function lireLignes(fichier: string) {
  const rl = readline.createInterface({
    input: fs.createReadStream(fichier),
    crlfDelay: Infinity,
  });

  for await (const ligne of rl) {
    filtrerFasta(ligne);
  }
}

function filtrerFasta(ligne: string) {
  if (ligne.substring(0, 2) === "AC") {
    console.log(">" + ligne.substr(2).replace(/\s/g, ""));
  } else if (ligne.substring(0, 2) === "  ") {
    console.log(ligne.replace(/\s/g, ""));
  }
}

On observe, dans l’invocation de program, des méthodes déclaratives, comme .argument, qui permet d’ajouter une définition d’argument à la commande (explication fournie par ChatGPT).

Ici les arguments peuvent être plusieurs, d’où la présence de l’opérateur ..., ou opérateur de reste, qui permet à la méthode d’accepter un nombre indéfini d’arguments. Ce sera le rôle de la méthode .action, à la ligne suivante, d’itérer sur la liste d’arguments et d’en détecter la fin.

Écrire le résultat dans un fichier

Enfin ce serait bien d’écrire le résultat dans un fichier avec une option -o (même si on peut toujours utiliser les possibilités de redirection du shell). Voici :

#!/usr/bin/env -S npx tsx

import { Command } from "commander";

// Lire une séquence
import * as fs from "fs";      
import * as readline from "readline"; 

// --- Programme principal avec Commander ---
const program = new Command();

program
    .name("lire-fasta")
    .description("Lire des fichiers séquentiels SwissProt et extraire les séquences Fasta dans un fichier")
    .argument("<fichiers...>", "Noms des fichiers à lire")
    .option("-o, --output <fichier>", "Rediriger la sortie vers un fichier")
    .action(async (fichiers: string[], options: { output?: string }) => {
	let outStream: fs.WriteStream | NodeJS.WriteStream;
	
	if (options.output) {
	    outStream = fs.createWriteStream(options.output, { flags: "w" });
	} else {
	    outStream = process.stdout;
	}
	
	for (const fichier of fichiers) {
	    console.error(`\n--- Lecture de ${fichier} ---`);
	    await lireLignes(fichier, outStream);
	}

	if (options.output) {
	    (outStream as fs.WriteStream).end();
	    console.error(`\nRésultats écrits dans ${options.output}`);
	}
  });

program.parse(); // ← déclenche le parsing des arguments

// --- Fonctions utilitaires ---
async function lireLignes(fichier: string, outStream: NodeJS.WriteStream | fs.WriteStream) {
    const rl = readline.createInterface({
        input: fs.createReadStream(fichier),
        crlfDelay: Infinity, // gère correctement \n et \r\n
    });

    for await (const ligne of rl) { 
        filtrerFasta(ligne, outStream);
      }
}

function filtrerFasta(ligne: string, outStream: NodeJS.WriteStream | fs.WriteStream) { 
    if (ligne.substring(0, 2) === "AC") {
        outStream.write(">" + ligne.substr(2).replace(/\s/g, "") + "\n");
    }
    else if (ligne.substring(0, 2) === "  ") {
        outStream.write(ligne.replace(/\s/g, "") + "\n");
    }
}

On notera, dans la déclaration de type du paramètre outStream des fonctions lireLignes et filtrerFasta ainsi que de la méthode .action la notation :

outStream: NodeJS.WriteStream | fs.WriteStream

C’est une particularité de TypeScript/JavaScript : ces fonctions et méthodes peuvent accepter, comme argument outStream, soit des objets de type fs.WriteStream, si l’option -o est présente et suivie d’un nom de fichier de sortie, soit des objets de type NodeJS.WriteStream dans le cas contraire (résultat sur la sortie standard).

Voici un exemple de résultat :

>Q5XGC8;Q28HK2;
MDKSELVQKAKLSEQAERYDDMAASMKAVTELGAELSNEERNLLSVAYKNVVGARRSSWR
VISSIEQKTEGNDKRQQMAREYREKVETELQDICKDVLGLLDKYLVPNATPPESKVFYLK
MKGDYYRYLSEVASGDSKQETVTCSQQAYQEAFEISKSEMQPTHPIRLGLALNFSVFYYE
ILNSPEKACSLAKSAFDEAIAELDTLNEESYKDSTLIMQLLRDNLTLWTSENQGEEADNA
EADN
>Q39291;
MPAFYGGKLTTFEDDEKESEYGYVRKVSGPVVVADGMAGAAMYELVRVGHDNLIGEIIRL
EGDSATIQVYEETAGLTVNDPVLRTHKPLSVELGPGILGNIFDGIQRPLKTIAKRSGDVY
IPRGVSVPALDKDCLWEFQPKDFVEGDTITGGDLYATVFENSLMQHHVALPPDAMGKITY
LAPAGQYSLKDTVLELEFQGVKKSFTMLQTWPVRTPRPVASKLAADTPLLTGQRVLDALF
PSVLGGTCAIPGAFGCGKTVISQALSKYSNSDAVVYVGCGERGNEMAEVLMDFPQLTMTL
PDGREESVMKRTTLVANTSNMPVAAREASIYTGITIAEYFRDMGYNVSMMADSTSRWAEA
LREISGRLAEMPADSGYPAYLAARLASFYERAGKVKCLGGPERNGSVTIVGAVSPPGGDF
SDPVTSATLSIVQVFWGLDKKLAQRKHFPSVNWLISYSKYSTALESFYEKFDSDFIDIRT
KAREVLQREDDLNEIVQLVGKDALAEGDKITLETAKLLREDYLAQNAFTPYDKFCPFYKS
VWMMRNIIHFYNLANQAVERGAGMDGQKISYSLIKHRLGDLFYRLVSQKFEDPAEGEDVL
VGKFKKLHDDLTSGFRNLEDETR
>Q40002;
ELVRVGHDSLIGEIIRLEGDSATIQVYEETAGLTVNDPVLRTKKPLSCELGPGILGNIFD
GIQRPLKTIAIKSRDVYIPRGVSVPALDKDQLWEFQPNKLGVGDNITNGDLYATVFENTL
MKHHIALPPGAMGKISYIAPAGQYSLQDTVLELEFQGIKKEFTMLHTWPVRTPRPVASKL
AADTPLLTGQRVLDALFPSVLGGTCAIPGAFGCGKTVISQALSKYSNSDTVVYVGCGERG
NEMAEVLMDFPQLTMTLPDGREESVMKRTTLVANTSNMPVAAREASIYTGITIAEYFRDM
GYNVSMMADSTSRWAEALREISGRLAEMPADSGYPAYLASRLASFYERAGKVQCLGSPDR
TGSVTIVGAVSPPGGDFSDPVTSATLSIVQVFWGLDKKLAQRKHFPSVNWLISYSKYSTA
LEGYYEKFDPGFIDMRTKAREVLQREDDLNEIVQLVGKDALGESDKITLETAKLLREDYL
AQNAFTPYDKYCPFYKSVWMMRNIIHFNQLANQAVERAANADGHKITYAVVKSRMGDLFY
RLVSQKFEDPAEGEDVLVAKFQKLYDDLTAGFRNLEDEAR

Dans la même rubrique