NEVER - mistä on kyse TypeScriptin oudoimmassa tyypissä? Osa 2/2

lokak. 17, 2023

NEVER - mistä on kyse TypeScriptin oudoimmassa tyypissä?

Blogin toisessa osassa (osa 2/2) keskitymme neverin rooliin tyyppisupistimissa (engl. type guard), unioneissa ja leikkauksissa. Lisäksi katsomme, mikä merkitys neverillä on funktioiden paluuarvon tyyppinä. Blogin ensimmäisen osan pääset lukemaan täältä.

Yleensä tulkki päättelee never-tyypin

Jotta voisimme ymmärtää never-tyypin merkityksen muuttujan tyyppinä, palataan vielä hetkeksi joukko-opillisiin käsitteisiin, kuten unioni, leikkaus (engl. intersection) ja tyhjä joukko.


Never osana unionia

Unionit määrittävät joukon tyyppejä, jotka kelpaavat muuttujan arvoksi.

New ParagraphVoiko union-tyyppien vaihtoehdoksi antaa siis myös never-tyypin? Teknisesti, kyllä voi, mutta siinä ei ole mitään mieltä, sillä kuten blogin ensimmäisessä osassa totesimme, never laajentaa jokaista muuta TypeScript-tyyppiä ja sisältyy näin ollen jo jokaiseen muuhun unionissa olevaan tyyppiin. Neverin merkitseminen unioniin on tarpeetonta ja TypeScript-tulkki pudottaa never-tyypin unionista erillisenä tyyppinä.


Alla oleva esimerkki on kuten yllä, mutta olemme lisänneet unioniin never-tyypin (rivi 1).

New Never ei esiinny TypeScript-tulkin virheilmoituksessa (rivi3), vaikka never on merkitty yhdeksi unionin tyypeistä (rivi1). Tulkki on päätellyt never-tyypin ulos unionista, koska never sisältyy jo kaikkiin muihin tyyppeihin.


Unioni voi supistua type guardeilla neveriksi

Never siis sisältyy kaikkiin unioneihin. Joissain tilanteissa unionin tyyppivaihtoehdot voivat supistua TypeScript tulkissa pelkäksi never-tyypiksi. 


Tällainen tilanne on type guardilla toteutettu type narrowing -koodi, jossa ehtolausekkeet poissulkevat muuttujan mahdollisia tyyppejä siten, ettei jäljellä ole yhtäkään mahdollista tyyppiä, ja näin siitä tulee never.


Tässä esimerkissä union-tyyppiä supistetaan (engl. type narrowing) tyyppivahteina (engl. type guard) toimivilla if-ehtolauseilla.

New ParagraphEnsin “string | number”-union -tyypistä (rivi 1) supistuu ensimmäisessä if-lauseessa pois string, sitten else if -osassa number, jolloin viimeisen else-blokin sisällä muuttuja ei voi enää olla yhtäkään unionissa suoraan ilmaistua tyyppiä. Tyyppi on supistunut never-tyypiksi.



Leikkaus ja never

Leikkaus (engl. intersection) tarkoittaa joukko-opissa kahden tai useamman joukon yhteisiä alkioita, eli niitä alkioita, jotka esiintyvät kaikissa keskenään leikattavissa joukoissa. Vastaavasti TypeScript-maailmassa leikkaus tarkoittaa kahden tai useamman unionin yhteisiä tyyppejä.


Tässä “
type c” (rivi 3) on unioneiden a ja b leikkaus

Mitä jos leikkauksesta ei jääkään mitään jäljelle, eli jos keskenään leikattavilla unioneilla ei ole yhtäkään yhteistä tyyppiä? Kun leikkauksen lopputulos on tyhjä unioni, sen tyyppi on never.

Conditional type ja never

Tässä osiossa puhumme ensimmäistä kertaa tyyppitason muuttujasta, jota kutsuttakoon tässä geneeriseksi tyypiksi (engl. generics). Geneerinen tyyppi on siis tyyppimaailman muuttuja, joka kantaa mukanaan tietoa muuttujan tyypistä. Geneerisen tyypin varsinainen tyyppi määrittyy geneeristä tyyppiä alustettaessa, jonka jälkeen tyyppi säilyy geneerisen tyypin mukana missä tahansa sitä käytetäänkin. Tämä blogi ei käsittele geneerisiä tyyppejä tämän syvemmin, mutta geneerisen tyypin ajatus pähkinän kuoressa on hyvä tietää ehdollisten tyyppien esimerkkiä ymmärtääksemme.


JavaScriptissä on ehtolauseita kuten if-lauseet, jotka käsittelevät vain arvoja, eivät tyyppejä. TypeScript tuo ohjelmointiin myös tyyppejä käsittelevät ehtolauseet, jotka käsittelevät vain tyyppejä.


TypeScript-maailmassa on siis mahdollista operoida tyyppien ehtolauseilla (engl. Conditional Type).

Conditional Type -määrittelyssä käytetään geneeristä tyyppiä, sekä tyypin laajennettua versiota tarkoittavaa extends-merkintää. Katso alla olevaa esimerkkiä.

Yläpuolen esimerkki määrittää Conditional Typen avulla tyypin HasFourLegs. Tyypille annetaan geneerisenä tyyppinä Animal. Tyypin ehto-osassa  tehdään päätelmä: jos geneerisenä tyyppinä annetulla tyypillä on property “legs: 4”, eli toisin sanoen, jos Animal on laajennus { legs: 4 } -tyypistä, Animal-tyyppi täyttää tyypin HasFourLegs ehdollisen tyypityksen kriteerit.


Huomaathan, ettei tyyppiehtolause tarkasta, onko Animal oikeasti tyyppiä Animal. Vaikka ehtolause päätyisi never-osaan, Animal-tyyppi on edelleen Animal-tyyppiä. Animal-tyyppi ei vain silloin ole tyypin { legs:4 } laajennus. Huomaathan myös, ettei numero 4 ole tässä tapauksessa arvo, vaan TypeScriptin literaalityyppi. Arvot ovat JavaScriptiä, tyypit puolestaan ovat TypeScriptiä mukaan lukien literaalityypit. Tämä olisi kenties helpompi huomata, jos laajennetestissä tarkistettaisiinkin laajennusta esimerkiksi tyypille { legs: number }. Sekä number, että literaalityyppi 4 kuuluvat tyyppimaailmaan. 


Esimerkissä määritellään union tyyppi Animals. Tämän jälkeen määritellään tyyppi FourLegs, johon kohdistetaan tyyppisupistus (Type Narrowing) Conditional Typen avulla. FourLegs-tyypin unioniin päätyvät ainoastaan HasFourLegs Conditional Typen ehdon täyttävät Animals-uniontyypin unionialkiot, eli kaikki ne alkiot, jotka olivat laajennuksia objektille { legs: 4 }.


FourLegs-tyypiksi tulisi never, mikäli Animals-unionissa ei olisi yhtään Conditional Type -ehdossa määritettyä alkiota.


Sekavaa? Ei huolta, ehdolliset tyypit eivät ole TypeScriptin joka päiväisiä rakennuspalikoita. Oikeastaan kaikkein hyödyllisintä lienee tunnistaa Conditional Typet geneeristen tyyppien ja extends-ilmaisujen avulla jonkun toisen kirjoittamasta koodista ja ymmärtää siitä mitä koodissa tapahtuu.

Never funktion paluuarvon tyyppinä

Funktion paluuarvon tyyppinä never tarkoittaa funktiota, joka ei koskaan suoriudu loppuun asti.

Tällaisia tapauksia on kaksi:

  • funktio jää ikuiseen kiertoon
  • funktio päätyy aina virheeseen



Void on eri tyyppi ja eri asia kuin never

Kun funktio ei palauta mitään, sen tyypiksi voidaan asettaa void.

Kun funktio ei palauta mitään joko tyhjän tai puuttuvan return-ilmauksen vuoksi, sen paluuarvon tyyppi on void. Palauttamattomuus on kumminkin eri asia kuin se, ettei funktio koskaan pääse loppuun asti. Jos funktio jää jokaisella suorituskerralla ikuiseen kiertoon tai jos funktio jokaisella suorituskerralla päätyy virheeseen, on funktion paluutyyppi aina never.


Alla oleva funktio jää aina ikuiseen kiertoon, joten sen paluutyyppi on never.

Alla oleva funktio päätyy jokaisella suorituskerralla virheeseen, joten sen paluutyyppi on never.

Entä jos funktio pääsee joskus loppuun ja joskus taas ei? Voiko paluutyyppi olla esimerkiksi number | never ? Teknisesti näin voisi kirjoittaa, eikä TypeScript-kääntäjä huomauttaisi virheestä. number | never funktion paluutyyppinä ei kuitenkaan olisi mielekästä, sillä kuten olemme todenneet, kaikki tyypit ovat jo valmiiksi never-tyypin laajennuksia. TypeScript-tulkki supistaa tyypin automaattisesti number-tyypiksi.


Joten mikäli funktio kykenee joissain tapauksissa palauttamaan jonkin arvon, sen tulkittu paluuarvo ei koskaan ole never. Sillä ei ole merkitystä, voiko funktio joskus jäädä myös ikuiseen kiertoon tai jos se voi joskus päätyä virheeseen. Mikäli loppuun pääsy ei ole estynyt jokaisella suorituskerralla, funktion paluutyyppi ei voi olla never.

isHeads()-funktion paluutyyppi on boolean siitä huolimatta, että funktio voi joskus jäädä ikuiseen kiertoon. Paluutyypin merkitseminen boolean | never -tyypiksi olisi mahdollista, mutta tarpeetonta.


Kiitos kun luit blogini NEVER - TypeScriptin oudoin tyyppi osat 1 & 2! Tässä oli paljon asiaa ja käsittelimme useita TypeScript-konsepteja, jotka tavalla tai toisella, etäisestikin, liittyivät never-tyyppiin. TypeScript sisältää paljon enemmän ominaisuuksia ja konsepteja, kuin mitä on välttämätöntä osata ja tunnistaa, jotta voisi kirjoittaa hyvää TypeScript-koodia.



Johan Stenroth

Consultant

Viimeisimmät kirjoitukset

Webscalen konsultteja.
03 May, 2024
Kysy konsultilta -blogisarjassa konsulttimme tekevät selkoa alan termeistä ja ilmiöistä. Vastaukset on mitoitettu sopimaan pieneenkin tiedonnälkään. Tällä kertaa selvitämme, miten FinOps auttaa pilvikustannuksissa?
Webscalen konsultteja.
26 Apr, 2024
Kysy konsultilta -blogisarjassa konsulttimme tekevät selkoa alan termeistä ja ilmiöistä. Vastaukset on mitoitettu sopimaan pieneenkin tiedonnälkään. Tällä kertaa selvitämme, mikä on AWS Landing Zone?
Webscalen konsultteja.
19 Apr, 2024
Kysy konsultilta -blogisarjassa konsulttimme tekevät selkoa alan termeistä ja ilmiöistä. Vastaukset on mitoitettu sopimaan pieneenkin tiedonnälkään. Tällä kertaa selvitämme, mitä on DevSecOps?
Webscalen konsultteja.
12 Apr, 2024
Kysy konsultilta -blogisarjassa konsulttimme tekevät selkoa alan termeistä ja ilmiöistä. Vastaukset on mitoitettu sopimaan pieneenkin tiedonnälkään. Tällä kertaa selvitämme, mikä on Serverless Framework?
Lisää kirjoituksia
Share by: