A close up of the word webscale on a white background

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

17. lokakuuta 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.

A computer screen with a few lines of code on it

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).

A blurred image of a text box that says ' error ' on it

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.

A purple screen with a few lines of code on it

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

A purple screen with a few lines of code on it

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.

A purple screen with a few lines of code on it
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ä.

A blurred image of a text box that says type animals

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.

A computer screen with a few lines of code on it

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.

A blurred image of a piece of code on a purple background

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

A blurred image of a piece of code on a white background

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.

A purple screen with a bunch of code on it

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.


A man in a grey sweater is smiling for the camera

Johan Stenroth

Consultant

Viimeisimmät kirjoitukset

21. helmikuuta 2025
Pilvipalvelut mahdollistavat niin nopeamman innovoinnin, resurssien tehokkaamman hallinnan kuin joustavammat liiketoimintamallitkin. Jotta pilven tarjoamat liiketoimintaedut pystytään hyödyntämään, tarvitaan pilven käyttöön kuitenkin suunnitelmallisuutta ja järjestelmällisyyttä.
19. helmikuuta 2025
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 Amazon Cognito?
13. helmikuuta 2025
Pilvipulssi tuo ajankohtaiset uutiset AWS- ja Azure-pilvimarkkinoilta suoraan asiantuntijoiltamme. Tutustu uuteen AWS CloudFormation Hooks -ominaisuuteen, joka auttaa varmistamaan, että infrastruktuurimuutokset noudattavat ennalta määriteltyjä sääntöjä ennen käyttöönottoa.
11. helmikuuta 2025
AWS DevOps Services Competency on merkittävä virstanpylväs yrityksellemme ja osoitus sitoutumisestamme korkealaatuisiin DevOps-palveluihin AWS-ympäristössä. Mutta mitä tämä tarkoittaa asiakkaillemme, ja miksi AWS Competency -ohjelma on tärkeä?
Lisää kirjoituksia
Share by: