Podstawy TypeScript #1- pierwsze kroki i konfiguracja (mój tsconfig.json)

TypeScript od absolutnego początku!

Nie ma w tym tajemnicy, że zakochałam się na zabój w TypeScriptcie i mam nadzieje, że uda mi się przekonać Was do spróbowania. Dzisiaj rozwiązemy pierwszy problem zniechacający początkujących do TypeScripta, czyli ustawienie konfiguracji.

Mam dla was dwa rozwiązania: po prostu skopiujcie cudzą konfigurację (w ostateczności moją) albo zapraszam do czytania całości posta gdzie staram sie prostym językiem wytłumaczyć mój tsconfig.json

Zaczynając ten tutorial powinieneś, drogi czytelniku, znać podstawy programowania w JavaScript i obiektowego.

Pierwsze kroki

TypeScript przetwarza się do JavaScriptu, więc musimy mieć możliwość do wykonania tej czynności.

  1. Zainstaluj stabilną wersją NodeJS z oficjalnej strony lub innego bezpiecznego źródła. Dzięki temu zyskasz środowisko uruchomieniowe wraz z popularnym managerem paczek npm.
  2. Zainstaluj globalnie TypeScripta z managera paczek:

npm install -g typescript

W tym tutorialu będziemy korzystać z edytora Visual Code, bo ma bardzo duże wsparcie dla TypeScripta. Ba! Jest w nim napisany :) Ale jeśli chcesz to możesz wybrać jakikolwiek inny :)

Stwórzmy sobie folder i otwórzmy go w VC.

W tym folderze utwórzmy dwa pliki: html oraz ts.

Plik html to będzie standardowa strona, która będzie zawierała plik Javscript.

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <script src="hi.js" async></script>
    <title>Event interface</title>
</head>

<body>
    <div>
        <button id="hi">
                Hi!
        </button>
    </div>
</body>

</html>

Teraz jakiś prosty skrypt (napisany w JavaScript) z rozszerzeniem TypeScript (*.ts).

// hi.ts
window.onload = function() {
    let button = document.getElementById('hi');
    alert('wczytany!');
};

Teraz w konsoli (można otworzyć ją w VC, gdzie od razu przeniesie nas do folderu (Ctrl/Command + ~) wpisujemy:

tsc hi.ts

Po zakończeniu powinien nam się wygenerować plik hi.js i osiągnęliśmy pierwszy sukces na naszej drodze do zostania mistrzem TypeScript'a.

Oczywiście nikt tego w ten sposób nie robi, ale fajnie znać podstawy :)

Tsconfig.json - konfiguracja

Czekajcie! Ale skąd polecenie tsc wiedziało co i gdzie ma wygenerować? Dlaczego przeszedł taki zwykły JavaScript?

Podstawową odpowiedzią jest plik konfiguracyjny (którego nota bene zabrakło w moim tutorialu z Node.js) TypeScript'a. Jeszcze nie stworzyliśmy takiego pliku,  dlatego są brane wartości domyślne np. generowanie plików w obecnym folderze :)

Jednak projekty są bardzo skompilowane, a Typescript służy do pisania przeróżnych aplikacji, więc musi istnieć miejsce w którym możemy sobie wszystko zdefiniować.

W celu podania konfiguracji musimy stworzyć plik o nazwie tsconfig.json. Na przykład:

{
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/",
    "moduleResolution": "node",
    "lib": ["es7", "es2015", "dom"],
    "target": "es5"
  },
  "include": ["./src/**/*", "public"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}

W tym przykładnie najciekawszy jest obiekt compilerOptions. Podajemy kolejno domyślną ścieżkę aplikacji, folder w którym ma się pojawić plik wynikowy (hi.js na przykład), jaki typ importów został użyty, biblioteki które się przydadzą oraz w jakiej wersji powinien być wynikowy JavaScript.

Nie przejmujcie się, jeśli coś wydaje się zbyt skomplikowane, zaraz wszystko wyjaśnię, a zwykle wystarczy skopiować plik do swojego projektu.

Mój tsconfig.json

Ten wpis będzie służył za punkt odniesienia do moich innych TypeScriptowych postów, bo często pytacie o konfigurację. Zwykle jest taka sama lub bardzo podobna, więc nie ma sensu zamieszczać jej co wpis :)

baseUrl

To ścieżka bazowa dla całej konfiguracji.

outDir

To ścieżka w której umieszczone będę wynikowe pliki.

moduleResolution

To właściwość od której zależy jak TypeScript zarządza modularnością (rozpoznaje moduły). Tu sprawa jest trochę grubsza i pewnie można by zrobić o tym osobny wpis. Jedyne co musicie jednak wiedzieć to, że tsc przyjmuje tu dwie stałe: node oraz classic. Typ klasyczny odchodzi do lamusa, a więcej możecie o tym poczytać tutaj.

noImplicitAny

W TypeScripcie można też deklarować typ poprzez przypisanie wartości. Na przykład:

(1) const x = 'napis';
(2) const x: string = 'napis2';

Domyślnie oba są poprawne, ale osobiście wolę, żeby w każdym moim projekcie był wymuszany sposób drugi.

removeComments

Najoczywistsza z opcji: w czasie kompilacji do JavaScript usuwa komentarze.

preserveConstEnums

Przy kompilacji transpiler zamienia poszczególne wartości stałych+ enumów na proste typy. Możemy chcieć zachować enum jako obiekt. Brzmi skomplikowanie? Oto przykład:

const enum Pojazdy {  
    Samochod = -1,
}

Domyślną wartością dla preserveConstEnums jest false, więc po kompilacji ten kod... zniknie. Zostaną przypisane jedynie wartości w miejscach gdzie się go używa. Jest to spora optymalizacja kodu, ale tracimy grunt pod nogami w Javascriptcie.

Jak go zachować? Usunąć const lub ustawić preserveConstEnums na true, żeby kompilował się normalnie. Znaczy jak na TypeScripta :)

var Pojazdy;  
(function (Pojazdy) {  
    Pojazdy[Pojazdy["Samochod"] = -1] = "Samochod";   
})(Pojazdy||(Pojazdy={}));

sourceMap

Generuje mapkę do zewnętrznego pliku. Ułatwia kodowanie, nie zapycha skryptu wynikowego. Nie wiesz co to source-mapka? Zajrzyj tutaj.

module

Skoro już w moduleResolution wiemy jak TypeScript rozwiązuje moduły w plikach .ts, to jeszcze musimy zdefiniować jaki typ pojawi się w plikach wynikowych.

target

Javascript, w sumie EMCAScript, ma wiele wersji i tutaj możemy zdefiniować w jakiej wersji będzie plik wynikowy.

allowJs

Wartość, która określi czy kompilator ma dopuszczać przetwarzanie JavaScriptu, bowiem nic nie stoi na przeszkodzie, żeby część aplikacji była w JavaScriptcie, a część w TypeScriptcie. Jest to bardzo wygodne, gdy chcesz skorzystać z zewnętrznych bibliotek albo powoli przenosić aplikację na TypeScripta.

allowSyntheticDefaultImports

Nie wszystkie biblioteki, szczególnie te napisane w JavaScripcie, mają domyślne exporty. Można umożliwić pobieranie globalnych jako modułu poprzez:

import * as myModule from 'myLibrary';

jeśli powyższa właściwość będzie ustawiona na true.

esModuleInterop

Przydatny do babela, dzięki niemu z powodzeniem można kompilować różne wersje i typy importów. Ustawia allowSyntheticDefaultImports na true, więc w moim pliku konfiguracyjnym jest pewna niekonsekwencja: dwa razy deklaruje to samo. Jest powodowana lenistwem, gdyż używam jednego tsconfiga do wielu aplikacji.

emitDecoratorMetadata i experimentalDecorators

Umożliwia korzystanie z dekoratorów. Nie będę się rozpisywać, bowiem jestem w trakcie przygotowania specjalnego wpisu o dekoratorach.

lib

Tablica dodatkowych (niemniej jednak podstawowych) bibliotek. Tutaj możemy dodać obsługę DOM (do pisania front-endu),  funkcji oraz metod znanych z ES6 czy ES7 czy pojedyńcze rozszerzenia znane z nowszych wersji TypeScripta.

paths

To obiekt w którym możemy sobie zdefiniować ścieżki, które będzie rozpoznawał Typescript. Umożliwia to skracanie importów, pisanie wzorców ścieżek.

"paths": {
    "Components/*": [
    	"src/components/*"
    ],
    "Entity/*": [
    	"src/entity/*"
    ],
    "*": ["types/*"]
}

include/exclude

Można wyłączyć niektóre ścieżki i pliki z kompilacji. Najczęściej include dotyczy jakiegoś katalogu src (lub kilku), exclude zewnętrznych bibliotek (także folderu node_modules), testów itd.

Mój tsconfig.json

Pisanie tego wpisu uświadomiło mnie jak bardzo naśmieciłam w swoim tsconfigu :) Obiecuję poprawę w przyszłości, ale na razie śmiało, bierzcie jeden z nich:

Mój podstawowy:

{
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/",
    "moduleResolution": "node",
    "noImplicitAny": true,
    "removeComments": true,
    "preserveConstEnums": true,
    "sourceMap": true,
    "module": "commonjs",
    "target": "es6",
    "allowJs": true,
    "esModuleInterop": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "lib": ["es7", "es2015", "dom"],
    "paths": {
      "Components/*": ["src/components/*"],
      "Entity/*": ["src/models/*"],
      "*": ["types/*"]
    }
  },
  "include": ["./src/**/*", "public"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}

A teraz przykładowy frontowy:

 {
     "compilerOptions": {
         "baseUrl": "./",
         "outDir": "./dist/",
         "moduleResolution": "node",
         "noImplicitAny": true,
         "removeComments": true,
         "preserveConstEnums": true,
         "sourceMap": true,
         "module": "commonjs",
         "target": "es5",
         "allowJs": true,
         "esModuleInterop": true,
         "emitDecoratorMetadata": true,
         "experimentalDecorators": true,
         "allowSyntheticDefaultImports": true,
         "lib": [
             "es7",
             "es2015",
             "dom"
         ],
         "paths": {
             "Components/*": [
                 "src/components/*"
             ],
             "Entity/*": [
                 "src/entity/*"
             ],
             "*": ["types/*"]
         }
     },
     "include": [
         "./src/**/*"
     ],
     "exclude": [
         "node_modules",
         "**/*.spec.ts"
     ]
 }

W webpacku również korzystam z tych konfiguracji poprzez plugin:

//  webpack.config.js
// ...
plugins: [new TsconfigPathsPlugin({ configFile: "./frontend.tsconfig.json" })],
// ...

Podsumowanie

Mam nadzieje, że ta seria pozwoli Wam się zakochać w TypeScriptcie, bo pisanie w nim to sama przyjemność, a konfiguracja nie jest taka straszna. Może i ci którzy trochę znają TypeScripta też znajdą coś dla siebie :)

W najbliższym czasie będę umieszczała trochę bardziej zaawansowanych wpisów o TypeScriptcie, ale zawsze chętnie wracam do podstaw, żeby utrwalić wiedzę.

Podobało się Wam? Popełniłam gdzieś błąd? Jakieś niedopowiedzenie? Coś powinnam zmienić w konfiguracji?

Jak zwykle zapraszam do dyskusji i pozdrawiam :)

Kamila

Dziękuję za poświęcony czas, będzie mi bardzo miło jak zostawisz komentarz :)