Skip to main content

Mikä TypeScript on?

TypeScriptin omilla sivuilla kerrotaan, että "TypeScript is JavaScript with syntax for types" (typescriptlang.org). TypeScriptin GitHub-sivulla puolestaan kieltä kuvaillaan seuraavasti:

"TypeScript is a language for application-scale JavaScript. TypeScript adds optional types to JavaScript that support tools for large-scale JavaScript applications for any browser, for any host, on any OS. TypeScript compiles to readable, standards-based JavaScript."

Microsoft. TypeScript at GitHub.com

TypeScriptiä luonnehditaankin seuraavissa kappaleissa edellä mainittujen ominaisuuksien kautta.

"JavaScript with syntax for types"

Eri ohjelmointikielissä on erilaisia lähestymistapoja arvojen tyyppien käsittelemiseksi. JavaScript-kielessä kaikilla arvoilla on olemassa jokin tyyppi, kuten numero, merkkijono, objekti tai taulukko. Tyypitys on kuitenkin dynaamista, eli muuttujiin voidaan asettaa vapaasti eri tyyppisiä arvoja ja funktiot voivat vastaanottaa ja niistä voidaan palauttaa eri tyyppisiä arvoja. Koska muuttujien, parametrien ja paluuarvojen tyypit riippuvat suoritusaikaisesta datasta, tyyppejä käsitellään ja mahdollisesti tarkastetaan ajonaikaisesti ohjelmaa suoritettaessa.

TypeScript tuo dynaamisesti tyypitettyyn JavaScriptiin tuen staattisille tyyppimäärityksille.

Dynaaminen tyypitys

Eri ohjelmointikielissä on erilaisia lähestymistapoja arvojen tyyppien käsittelemiseksi. JavaScript-kielessä kaikilla arvoilla on olemassa jokin tyyppi, kuten number, string, object tai array. Tyypitys on kuitenkin dynaamista, eli muuttujiin voidaan asettaa vapaasti eri tyyppisiä arvoja ja funktiot voivat vastaanottaa ja niistä voidaan palauttaa eri tyyppisiä arvoja:

"JavaScript is a dynamic language with dynamic types. Variables in JavaScript are not directly associated with any particular value type, and any variable can be assigned (and re-assigned) values of all types."

MDN. JavaScript data types and data structures

Koska muuttujien, parametrien ja paluuarvojen tyypit riippuvat suoritusaikaisesta datasta, tyyppejä käsitellään vain ajonaikaisesti ohjelmaa suoritettaessa.

Yksi dynaamisen tyypityksen heikkous, johon TypeScript pyrkii vastaamaan, on tyyppien tarkastaminen jo ennen koodin suorittamista. Katsotaan esimerkiksi seuraavaa JavaScript-kielistä esimerkkikoodia, jossa etsitään numeroita sisältävän taulukon suurinta arvoa Math.max-metodin avulla. Mitä seuraava koodi tulostaa?

buginen esimerkki
demo.js
let numbers = [42, 0, -1, 100, 9];
let largest = Math.max(numbers);

console.log({ largest });

Mitä yllä oleva koodi tulostaa? Mikä bugi koodissa on?

Yllä oleva koodiesimerkki tulostaa hieman yllättäen { largest: NaN }. Tämä johtuu siitä, että Math.max odottaa saavansa joukon arvoja erillisinä parametreina, eikä taulukkona. Paluuarvo on tavallaan perusteltu, koska annettu arvo ei ole numero.

Tämänkaltaiset bugit voivat olla yllättävän työläitä selvittää, koska metodi ei aiheuttanut poikkeusta tai "kaatanut" ohjelmaa, vaan se vain antoi paluuarvoksi ei-toivotun arvon. Tämä bugi ilmenisikin todennäköisesti ohjelmassa vasta myöhemmässä vaiheessa, kun metodin palauttamaa arvoa käytetään laskuoperaatiossa tai tulosteessa.

Virheellisiin metodikutsuihin ja tyyppeihin liittyvät ongelmat voidaan tyypillisesti havaita ja korjata jo koodia kirjoitettaessa, mikäli funktioioden parametrit sekä muuttujat tyypitetään staattisesti.

Staattinen tyypitys

Staattista tyypitystä hyödynnettäessä ohjelman muuttujille, parametreille ja paluuarvoille määritellään etukäteen tyypit, joita hyödynnetään käännösvaiheessa ohjelmakoodin oikeellisuuden tarkastamiseksi.

TypeScript on JavaScript-kielen laajennos, joten edellinen JS-koodiesimerkki voidaan kirjoittaa täsmälleen samalla tavalla TypeScript-kielellä:

Buginen esimerkki
demo.ts
let numbers = [42, 0, -1, 100];
let largest = Math.max(numbers);

console.log({ largest });

Tällä kertaa bugi löytyy jo koodia käännettäessä!

Kun tätä TypeScript-koodiesimerkkiä käännetään, havaitaan virhe jo käännösvaiheessa:

käännösvirhe

Käännetään lähdekoodi:

npx tsc demo.ts     # TypeScript asennettu paikallisesti
tsc demo.ts # TypeScript asennettu globaalisti

Kääntäjä huomaa virheen ja tulostaa seuraavan ilmoituksen:

error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'number'.

2 let largest = Math.max(numbers);
~~~~~~~

💡 TypeScriptin kääntämistä ja työkaluja käsitellään tarkemmin työkalut-osiossa.

TypeScript-kääntäjä, eli tsc, havaitsi yllä virheen, jossa Math.max-metodille annettiin numeron sijasta numerotaulukko. Metodille olisikin pitänyt antaa erillisiä numeroita, esimerkiksi. Math.max(42, 0, -1, 100), eikä taulukkoa Math.max(numbers).

Käytännössä tulet kirjoittamaan TypeScript-koodia editorilla, joka tarkastaa koodia jo sitä kirjoitettaessa. Editorisi siis varoittaa todennäköisesti virheistä jo ennen kuin ehdit itse kääntää koodiasi.

"Optional types"

Jos katsot tarkemmin edellä esitettyä demo.ts-esimerkkikoodia, huomaat, että siinä ei itseasiassa ole määritetty lainkaan tyyppejä, vaikka TypeScriptiä juuri väitettiin staattisesti tyypitetyksi kieleksi 🤔. Tyyppien määritteleminen itse ei olekaan monessa tapauksessa tarpeen, koska TypeScript osaa päätellä arvojen tyypit esimerkiksi sijoitusperaatioiden ja return-lauseiden perusteella. Tyyppien päättelemisestä käytetään tarkemmin termiä Type Inference.

Koska numbers-muuttujaan asetetaan taulukko, joka sisältää vain numeroita, päättelee TypeScript sen tyyliksi numerotaulukon eli number[]. TypeScriptin päättelemät tyypit näkyvät mm. ylempänä virheilmoituksessa "'number[]' is not assignable to parameter of type 'number'".

Bugin korjaus

Math.max-metodille täytyy antaa parametrina taulukon sijasta erilliset numerot. Tämä saadaan ratkaistua siten, että numerotaulukko number[] puretaan erillisiksi arvoiksi JavaScriptin spread-operaattorilla. Metodin kutsusta tulee siis Math.max(...numbers). Pidemmin kirjoitettuna ja tyyppimääritysten kera koodi saadaan siis korjattua seuraavasti:

Korjattu koodi
demo.ts
let numbers: number[] = [42, 0, -1, 100];
let largest: number = Math.max(...numbers);

console.log({ largest });

Tämä korjattu versio tulostaa odotetusti { largest: 100 }.

"Application-scale JavaScript"

TypeScriptin käsikirjassa todetaan osuvasti, että JavaScriptin käyttö on viime vuosikymmeninä laajentunut pienistä verkkosivuille kehitettävistä interaktiivisista elementeistä miljoonien koodirivien kokoisiksi ohjelmiksi. Samalla JavaScript-kielen kyky tukea suurempia projekteja sekä niissä esiityviä monimutkaisia sisäisiä suhteita ei ole kehittynyt.

Pienemmissä ohjelmistoprojekteissa TypeScriptin käyttöönotto voi tuntua turhalta ja peräti ylimääräiseltä työltä. Suurempia kokonaisuuksia hallittaessa se kuitenkin helpottaa ohjelmistojen kehittämistä merkittävästi. Kääntäjä havaitsee koodin muutosten, poistamisen ja lisäysten vaikutukset, ja löytää mahdollisia virheitä niihin liittyen ohjelmiston eri osien välillä. Ohjelmistojen riippuvuuksia voidaan myös päivittää huolettomammin, kun kääntäjä tarkistaa jokaisen rivin automaattisesti.

"Any browser, any host, any OS"

TypeScript-kääntäjä kääntää TypeScript-kielisen lähdekoodin standardin mukaiseksi JavaScript-koodiksi, jota voidaan suorittaa missä vain JavaScript-suoritusympäristössä, esimerkiksi selaimessa tai Node.js:llä. TypeScript tukee myös eri ECMAScript-versioita, joten voit halutessasi kääntää nykyaikaista syntaksia hyödyntävän koodisi myös vanhempien selainten tukemaan muotoon.

Käytännössä JavaScript-koodin suoritusympäristöön vaikuttaa monta tekijää. Esimerkiksi selaimessa on käytössä WHATWG-spesifikaation mukaiset ominaisuudet kuten "dom". Vastaavasti Node.js-ympäristössä on oma standardikirjastonsa. TypeScript osaa ottaa nämä huomioon ja tarkastaa tyypit, kunhan ECMAScript-versio määritellään target-asetuksella ja kirjastot lib-asetuksella. Asetukset voidaan tallentaa projektin tsconfig.json-tiedostoon.

"TypeScript compiles to JavaScript"

Kääntäjän tuottama JavaScript-koodi on "puhdasta" JavaScriptiä, eikä siinä ole merkkejä TypeScriptistä.

"Roughly speaking, once TypeScript’s compiler is done with checking your code, it erases the types to produce the resulting "compiled" code. This means that once your code is compiled, the resulting plain JS code has no type information."

Microsoft. TypeScript for the New Programmer. typescriptlang.org

Jos palaamme vielä ylempänä esitettyyn esimerkkiin, se voidaan kääntää asetuksista riippuen erilaisiin muotoihin.

demo.ts
let numbers: number[] = [42, 0, -1, 100];
let largest: number = Math.max(...numbers);

console.log({ largest });

Yllä oleva koodi näyttää käännettynä ES2022-standardin mukaiseksi JavaScriptiksi seuraavalta:

compiledES2022.js
let numbers = [42, 0, -1, 100];
let largest = Math.max(...numbers);

console.log({ largest });

Koska ES2022 tukee käytännössä kaikkia koodissa esiintyviä ominaisuuksia, ei käännetty koodi eroa juurikaan alkuperäisestä. Vain siinä esiintyvät tyypit on poistettu.

Mikäli koodia on tarkoitus suorittaa vanhemmilla selaimilla, jotka eivät esimerkiksi tue koodissa käytettyjä "spread"- ja "object property shorthand"-syntakseja, voidaan se kääntää esimerkiksi ES3:n tukemaan muotoon:

compiledES3.js
var numbers = [42, 0, -1, 100];
var largest = Math.max.apply(Math, numbers);

console.log({ largest: largest });

Tässä viimeisessä käännetyssä versiossa muuttujat on määritetty aikaisemmista esimerkeistä poiketen vanhemmalla var-avainsanalla ja Math.max-metodin kutsu sekä { largest: largest } on esitetty pidemmässä muodossa:

- let numbers: number[] = [42, 0, -1, 100];
+ var numbers = [42, 0, -1, 100];

- let largest: number = Math.max(...numbers);
+ var largest = Math.max.apply(Math, numbers);

- console.log({ largest });
+ console.log({ largest: largest });

Voit kokeilla itse kääntää tätä koodia eri asetuksilla TypeScript playground -palvelussa.