Rajapinnat vs tyypit TypeScriptissä


Mikä on ero näiden lauseiden (interface vs type) välillä TypeScriptissä?

interface X {
    a: number
    b: string
}

type X = {
    a: number
    b: string
};

Vastaus #1

2019 päivitys


Nykyiset vastaukset ja virallinen dokumentaatio ovat vanhentuneita. Ja niille, jotka ovat uusia TypeScriptissä, käytetty terminologia ei ole selkeää ilman esimerkkejä. Alla on luettelo ajantasaisista eroista.

1. Objektit/Funktiot

Molempia voidaan käyttää kuvaamaan objektin muotoa tai funktion allekirjoitusta. Mutta syntaksi on erilainen.

Rajapinta

interface Point {
  x: number;
  y: number;
}

interface SetPoint {
  (x: number, y: number): void;
}

Tyyppi alias

type Point = {
  x: number;
  y: number;
};

type SetPoint = (x: number, y: number) => void;

2. Muut tyypit

Toisin kuin rajapinnan, tyyppialiasia voidaan käyttää myös muihin tyyppeihin, kuten primitiiveihin, unioneihin ja tupleihin.

// primitive
type Name = string;

// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };

// union
type PartialPoint = PartialPointX | PartialPointY;

// tuple
type Data = [number, string];

3. Laajenna

Molempia voidaan laajentaa, mutta jälleen syntaksi eroaa. Lisäksi huomaa, että rajapinta ja tyyppialias eivät ole toisensa poissulkevia. Rajapinta voi laajentaa tyyppialiasia, ja päinvastoin.

Rajapinta laajentaa rajapintaa

interface PartialPointX { x: number; }
interface Point extends PartialPointX { y: number; }

Tyyppialias laajentaa tyyppialiaasia

type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };

Rajapinta laajentaa tyyppialiasin

type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }

Tyyppialias laajentaa rajapintaa

interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; };

4. Välineet

Luokka voi toteuttaa rajapinnan tai tyyppialiasin, molemmat täsmälleen samalla tavalla. Huomaa kuitenkin, että luokkaa ja rajapintaa pidetään staattisina mallikuvina. Siksi ne eivät voi toteuttaa/laajentaa tyyppialiasia, joka nimeää unionityypin.

interface Point {
  x: number;
  y: number;
}

class SomePoint implements Point {
  x = 1;
  y = 2;
}

type Point2 = {
  x: number;
  y: number;
};

class SomePoint2 implements Point2 {
  x = 1;
  y = 2;
}

type PartialPoint = { x: number; } | { y: number; };

// FIXME: can not implement a union type
class SomePartialPoint implements PartialPoint {
  x = 1;
  y = 2;
}

5. Deklaraation yhdistäminen

Toisin kuin tyyppialias, rajapinta voidaan määritellä useita kertoja, ja sitä käsitellään yhtenä rajapintana (kaikkien määritelmien jäsenet yhdistetään).

// These two declarations become:
// interface Point { x: number; y: number; }
interface Point { x: number; }
interface Point { y: number; }

const point: Point = { x: 1, y: 2 };

Vastaus #2

Päivitys maaliskuu 2021: Uudempi TypeScript-käsikirja (mainittu myös nju-clc:n vastauksessa alla) sisältää osion 'Rajapinnat vs. Tyyppialiakset', joka selittää erot.


Alkuperäinen vastaus (2016)

Kuten (nyt arkistoitu) TypeScriptin kielispesifikaatio määrää:

Toisin kuin rajapinnan määrittely, joka aina esittelee nimettyä objektityyppiä, tyyppialias-määrittely voi antaa nimen minkä tahansa tyyppiselle tyypille, mukaan lukien primitiivi-, yhdistelmä- ja leikkaustyypit.

Määrityksessä mainitaan edelleen:

Rajapintatyypit muistuttavat monin tavoin tyyppialiasia objektityyppilitteraaleille, mutta koska rajapintatyypit tarjoavat enemmän ominaisuuksia, niitä pidetään yleensä parempina kuin tyyppialiasia. Esimerkiksi rajapintatyyppi

interface Point {
    x: number;
    y: number;
}

voitaisiin kirjoittaa tyypin aliaksena

type Point = {
    x: number;
    y: number;
};

Kuitenkin sen tekeminen tarkoittaa, että seuraavat ominaisuudet menetetään:

  • Rajapinta voidaan nimetä extends- tai implements-lauseessa, mutta tyypin alias ei voi olla olion tyyppi-literalille Ei enää totta TS 2.7:sta lähtien.
  • Rajapinnalla voi olla useita yhdistettyjä määritelmiä, mutta tyypin alias ei voi olla objektityypin literalille.

Vastaus #3

Merkityksellinen vuonna 2021

TypeScript-versiolle: 4.3.4


Yhteenveto;

Henkilökohtainen käytäntönik, jonka kuvasin alla, on tämä:

Aina suositse interface-määrettä type-määreen sijaan.

Milloin käyttää type:

  • Käytä type määritellessäsi aliasia primitiivityypeille (string, boolean, number, bigint, symbol, jne.)
  • Käytä type, kun määrittelet tuple-tyyppejä
  • Käytä type, kun määrittelet funktiotyyppejä
  • Käytä type, kun määrittelet yhdistelmän
  • Käytä type, kun yrität ylikuormittaa funktioita objektityypeissä yhdistämisen kautta
  • Käytä type, kun tarvitsee hyödyntää kartoitettyjä tyyppejä

Milloin käyttää interface:

  • Käytä interface kaikille olio-tyypeille, joissa type-käyttö ei ole vaadittua (katso yllä)
  • Käytä interface, kun haluat hyödyntää deklarointiyhdistämistä.

Primitiiviset tyypit

Helpoin ero nähdä type- ja interface-avainsanojen välillä on se, että vain type-avainsanaa voidaan käyttää primitiivisen tyypin aliasoinnissa:

type Nullish = null | undefined;
type Fruit = 'apple' | 'pear' | 'orange';
type Num = number | bigint;

Yhtäkään näistä esimerkeistä ei ole mahdollista saavuttaa käyttöliittymien avulla.

💡 Kun annat primitiivisen arvon tyyppialiasin, käytä type avainsanaa.

Tuple-tyypit

Tupleja voidaan tyypittää vain käyttäen type-avainsanaa:

type row = [colOne: number, colTwo: string];

💡 Käytä type-avainsanaa määrittäessäsi tyyppejä tupleille.

Funktiotyypit

Funktioita voidaan kirjoittaa sekä type- että interface-avainsanoilla:

// via type
type Sum = (x: number, y: number) => number;

// via interface
interface Sum {
  (x: number, y: number): number;
}

Koska sama vaikutus voidaan saavuttaa kummallakin tavalla, sääntö tulee olemaan käyttää type näissä tilanteissa, koska se on hieman helpompi lukea (ja vähemmän sanallinen).

💡 Käytä type-avainsanaa, kun määrität funktiotyyppien tyyppejä.

Yhdistelmälajit

Liittotyyppejä voidaan saavuttaa vain käyttämällä type-avainsanaa:

type Fruit = 'apple' | 'pear' | 'orange';
type Vegetable = 'broccoli' | 'carrot' | 'lettuce';

// 'apple' | 'pear' | 'orange' | 'broccoli' | 'carrot' | 'lettuce';
type HealthyFoods = Fruit | Vegetable;

💡 Kun määritellään yhdistelmätipuja, käytä type-avainsanaa

Objektilajit

JavaScriptin objekti on avain/arvo-kartta, ja "object type" on Typescriptin tapa määritellä näiden avain/arvo-karttojen tyypit. Sekä interface että type voidaan käyttää määriteltäessä tyyppiä objektille, kuten alkuperäinen kysymys selvästi osoittaa. Joten milloin käytät type vs interface objektityypeille?

Leikkaus vs Perintö

Tyyppien ja koostumuksen kanssa voin tehdä jotain tällaista:

interface NumLogger { 
    log: (val: number) => void;
}
type StrAndNumLogger = NumLogger & { 
  log: (val: string) => void;
}

const logger: StrAndNumLogger = {
  log: (val: string | number) => console.log(val)
}

logger.log(1)
logger.log('hi')

Typescript on täysin tyytyväinen. Entä jos yritän laajentaa sitä rajapinnalla:

interface StrAndNumLogger extends NumLogger { 
    log: (val: string) => void; 
};

StrAndNumLogger-määrittely antaa minulle virheen:

Interface 'StrAndNumLogger' incorrectly extends interface 'NumLogger'

Rajapintojen kanssa alityyppien on täsmälleen vastattava ylityypissä ilmoitettuja tyyppejä, muuten TS heittää virheen kuten yllä oleva.

💡 Kun yrität ylikuormittaa funktioita oliotyypeissä, sinun on parempi käyttää type-avainsanaa.

Ilmoitusten yhdistäminen

Keskeinen asia TypeScriptin rajapinnoissa, joka erottaa ne tyypeistä, on se, että niitä voidaan laajentaa uudella toiminnallisuudella sen jälkeen, kun ne on jo määritelty. Yleinen käyttötapa tälle ominaisuudelle ilmenee, kun haluat laajentaa node-moduulista vietyjä tyyppejä. Esimerkiksi @types/jest vie tyyppejä, joita voidaan käyttää työskenneltäessä jest-kirjaston kanssa. Kuitenkin jest sallii myös päätyypin jest laajentamisen uusilla funktioilla. Esimerkiksi voin lisätä mukautetun testin näin:

jest.timedTest = async (testName, wrappedTest, timeout) =>
  test(
    testName,
    async () => {
      const start = Date.now();
      await wrappedTest(mockTrack);
      const end = Date.now();

      console.log(`elapsed time in ms: ${end - start}`);
    },
    timeout
  );

Ja sitten voin käyttää sitä näin:

test.timedTest('this is my custom test', () => {
  expect(true).toBe(true);
});

Ja nyt sen testin kulunut aika tulostetaan konsoliin, kun testi on valmis. Hienoa! On vain yksi ongelma - TypeScript ei tiedä, että olen lisännyt timedTest-funktion, joten se antaa virheen editorissa (koodi toimii kuitenkin oikein, mutta TS tulee olemaan vihainen).

Ratkaistakseni tämän minun täytyy kertoa TS:lle, että olemassa olevien jestistä saatavilla olevien tyyppien päälle on uusi tyyppi. Tehdäkseni sen, voin tehdä näin:

declare namespace jest {
  interface It {
    timedTest: (name: string, fn: (mockTrack: Mock) => any, timeout?: number) => void;
  }
}

Koska rajapinnat toimivat näin, tämä tyypin määrittely yhdistetään tyyppimäärittelyjen kanssa, jotka on viety paketista @types/jest. Joten en vain uudelleenmääritellyt jest.It-tyyppiä; laajensin jest.It-tyyppiä uudella funktiolla, jotta TS tunnistaa nyt oman mukautetun testifunktiollani.

Tämän tyyppinen asia ei ole mahdollista type-avainsanan kanssa. Jos @types/jest olisi ilmoittanut tyypinsä type-avainsanalla, en olisi voinut laajentaa näitä tyyppejä omilla mukautetuilla tyypeilläni, eikä siis olisi ollut hyvää tapaa tehdä TS:ää tyytyväiseksi uuden funktionsa kanssa. Tätä prosessia, joka on ainutlaatuinen interface-avainsanalle, kutsutaan deklarointien yhdistämiseksi.

Deklarointien yhdistäminen on mahdollista tehdä myös paikallisesti näin:

interface Person {
  name: string;
}

interface Person {
  age: number;
}

// no error
const person: Person = {
  name: 'Mark',
  age: 25
};

Jos tekisin täsmälleen saman asian yllä type-avainsanan kanssa, saisin virheen, koska tyyppejä ei voi uudelleenjulistaa/yhdistää. Todellisessa maailmassa JavaScript-objektit ovat hyvin samanlaisia kuin tämä interface-esimerkki; niitä voidaan päivittää dynaamisesti uusilla kentillä ajon aikana.

💡 Koska rajapintailmoituksia voidaan yhdistää, rajapinnat kuvaavat JavaScript-olioiden dynaamista luonnetta tarkemmin kuin tyypit, ja niiden käyttöä tulisi suosia tästä syystä.

Kartoitettu objektityyppi

Käyttämällä type-avainsanaa, voin hyödyntää muunnettuja tyyppejä näin:

type Fruit = 'apple' | 'orange' | 'banana';

type FruitCount = {
  [key in Fruit]: number;
}

const fruits: FruitCount = {
  apple: 2,
  orange: 3,
  banana: 4
};

Tätä ei voida tehdä rajapintojen kanssa:

type Fruit = 'apple' | 'orange' | 'banana';

// ERROR: 
interface FruitCount {
  [key in Fruit]: number;
}

💡 Kun tarvitsee hyödyntää kartoitettuja tyyppejä, käytä type -avainsanaa

Suorituskyky

Useimmiten yksinkertainen tyypin alias objektityypille toimii hyvin samankaltaisesti kuin rajapinta.

interface Foo { prop: string }

type Bar = { prop: string };

Kuitenkin, ja heti kun sinun tarvitsee yhdistää kahta tai useampaa tyyppiä, sinulla on mahdollisuus laajentaa näitä tyyppejä rajapinnalla tai yhdistää ne tyypin aliasissa, ja silloin erot alkavat olla merkityksellisiä.

Rajapinnat luovat yhden tasaisen oliotyypin, joka havaitsee ominaisuuksien ristiriidat, jotka on yleensä tärkeää ratkaista! Leikkaukset sen sijaan yhdistävät ominaisuuksia rekursiivisesti, ja joissakin tapauksissa tuottavat never-tyypin. Rajapinnat myös näkyvät johdonmukaisesti paremmin, kun taas tyyppialiasat leikkauksiin eivät voi näkyä osana muita leikkauksia. Rajapintojen väliset tyyppisuhteet myös välimuistitetaan, toisin kuin leikkaustyypit kokonaisuutena. Viimeinen huomionarvoinen ero on, että kohdeleikkaustyyppiä tarkistettaessa jokainen osa tarkistetaan ennen kuin tarkistetaan "tehokasta"/"tasoitetttua" tyyppiä vastaan.

Tästä syystä tyyppien laajentamista rajapinnoilla/extends-suureella suositellaan leikkaustyyppien luomisen sijaan.

Myös TypeScriptin dokumentaatiosta

Tuosta huolimatta suosittelemme käyttämään rajapintoja tyyppialiaiden sijaan. Erityisesti siksi, että saat parempia virheilmoituksia. Jos viet hiiren seuraavien virheiden päälle, näet, kuinka TypeScript voi tarjota tiiviimpiä ja tarkemmin kohdennettuja viestejä työskennellessäsi rajapintojen, kuten Chickenin, kanssa.

Lisää Typescript-wikissä.

Vastaus #4

TypeScript 3.2:n (marras 2018) lähtien seuraava on totta:

NäkökulmaTyyppiKäyttöliittymä
Voi kuvata toimintoja✅✅
Voi kuvata konstruktorit✅✅
Voi kuvata tupleja✅✅
Rajapinnat voivat laajentaa sitä⚠️✅
Luokat voivat laajentaa sitä🚫✅
Luokat voivat toteuttaa sen (implements)⚠️✅
Voi leikata toisen samanlaisen kanssa✅⚠️
Voi luoda liiton toisen oman lajinsa kanssa✅🚫
Voidaan käyttää luomaan kartoitettuja tyyppejä✅🚫
Voi kartoittaa kartoitettujen tyyppien avulla✅✅
Laajentuu virheilmoituksissa ja lokitiedoissa✅🚫
Voidaan lisätä🚫✅
Voi olla rekursiivinen⚠️✅

⚠️ Joissakin tapauksissa

Vastaus #5


Milloin käyttää type?


Geneeriset muunnokset

Käytä type, kun muunnet useita tyyppejä yhdeksi geneeriseksi tyypiksi.

Esimerkki :

type Nullable<T> = T | null | undefined
type NonNull<T> = T extends (null | undefined) ? never : T

Tyyppialiasointi

Voimme käyttää type-avainsanaa luodaksemme alias-nimiä pitkille tai monimutkaisille tyypeille, jotka ovat vaikeita lukea ja joiden kirjoittaminen uudelleen on myös hankalaa.

Esimerkki :

type Primitive = number | string | boolean | null | undefined

Tällaisen aliasin luominen tekee koodista tiiviimpää ja luettavampaa.


Tyyppien sieppaus

Käytä type saadaksesi selville objektin tyypin, kun tyyppi on tuntematon.

Esimerkki :

const orange = { color: "Orange", vitamin: "C"}
type Fruit = typeof orange
let apple: Fruit

Tässä saamme tuntemattoman tyypin orange, kutsumme sitä Fruit-tyypiksi ja käytämme sitten Fruit-tyyppiä luodaksemme uuden tyyppiturvallisen objektin apple.


Milloin käyttää interface?


Polymorfismi

interface on sopimus tietorakenteen muodon toteuttamisesta. Käytä interfacea tehdäksesi selväksi, että sen on tarkoitus toteuttaa ja käyttää sopimuksena siitä, miten objektia käytetään.

Esimerkki :

interface Bird {
    size: number
    fly(): void
    sleep(): void
}

class Hummingbird implements Bird { ... }
class Bellbird implements Bird { ... }

Vaikka voit käyttää type-tunnistetta tämän saavuttamiseksi, Typescriptiä pidetään enemmän oliopohjaisena kielenä ja interface:llä on erityinen paikka oliopohjaisissa kielissä. Koodin lukeminen interface:n kanssa on helpompaa, kun työskentelet tiimissä tai osallistut avoimen lähdekoodin yhteisöön. Se on myös helppoa uusille ohjelmoijille, jotka tulevat muista oliopohjaisista kielistä.

Virallinen Typescript-dokumentaatio sanoo myös:

... suosittelemme käyttämään interface-määritystä type-aliasin sijaan, kun mahdollista.

Tämä viittaa myös siihen, että type on tarkoitettu enemmän tyyppialiaattien luomiseen kuin itse tyyppien luomiseen.


Ilmoituksen yhdistäminen

Voit käyttää interface-rajapinnan deklarointien yhdistämisominaisuutta lisätäksesi uusia ominaisuuksia ja metodeja jo olemassa olevaan interface-rajapintaan. Tämä on hyödyllistä kolmansien osapuolten kirjastojen ambient-tietotyyppien deklaroinneissa. Kun kolmannen osapuolen kirjastosta puuttuu joitakin deklarointeja, voit määritellä rajapinnan uudelleen samalla nimellä ja lisätä uusia ominaisuuksia ja metodeja.

Esimerkki :

Voimme laajentaa yllä olevaa Bird-rajapintaa sisällyttämään uusia määritelmiä.

interface Bird {
    color: string
    eat(): void
}

Siinä se! On helpompi muistaa, milloin käyttää mitäkin, kuin eksyä kahden hienovaraisten erojen välillä.

Vastaus #6

TypeScriptin käsikirja antaa vastauksen:

Melkein kaikki käyttöliittymän ominaisuudet ovat saatavilla tyypissä.
Keskeinen ero on, että tyyppiä ei voi avata uudelleen uusien ominaisuuksien lisäämiseksi, toisin kuin käyttöliittymää, jota voidaan aina laajentaa.

Vastaus #7

Päivitys huhtikuu 2023:

https://www.typescriptlang.org/docs/handbook/2/types-from-types.html (Siirry uudelle sivulle)


https://www.typescriptlang.org/docs/handbook/advanced-types.html (Tämä sivu on vanhentunut)

Yksi ero on se, että rajapinnat luovat uuden nimen, jota käytetään kaikkialla. Tyyppialiasit eivät luo uutta nimeä — esimerkiksi virheilmoitukset eivät käytä alias-nimeä.

Vastaus #8

Esimerkkejä tyypeistä:

// luo puurakenne objektia varten. Et voi tehdä samaa rajapinnalla sen leikkaus (&) puutteen vuoksi

type Tree<T> = T & { parent: Tree<T> };

// tyyppiä käytetään rajoittamaan muuttuja siten, että siihen voidaan antaa vain muutama arvo. Rajapintojen (interface) kanssa ei ole yhdistelmää (|)

type Choise = "A" | "B" | "C";

// kiitos tyyppien, voit määritellä NonNullable-tyypin ehdollisen mekanismin avulla.

type NonNullable<T> = T extends null | undefined ? never : T;

Esimerkkejä käyttöliittymästä:

// voit käyttää rajapintaa OOP:ssa ja käyttää 'implements'-avainsanaa määrittääksesi objektin/luokan rungon

interface IUser {
    user: string;
    password: string;
    login: (user: string, password: string) => boolean;
}

class User implements IUser {
    user = "user1"
    password = "password1"

    login(user: string, password: string) {
        return (user == user && password == password)
    }
}

// voit laajentaa rajapintoja muilla rajapinnoilla

    interface IMyObject {
        label: string,
    }

    interface IMyObjectWithSize extends IMyObject{
        size?: number
    }

Vastaus #9

Muut vastaukset ovat loistavia! Muutamia muita asioita, joita Type voi tehdä mutta Interface ei voi

Voit käyttää unionia tyypissä

type Name = string | { FullName: string };

const myName = "Jon"; // works fine

const myFullName: Name = {
  FullName: "Jon Doe", //also works fine
};

Union-ominaisuuksien iteroiminen tyypissä

type Keys = "firstName" | "lastName";

type Name = {
  [key in Keys]: string;
};

const myName: Name = {
  firstName: "jon",
  lastName: "doe",
};

Tyypin leikkaus (kuitenkin myös tuettu rajapinnassa extends-avainsanalla)

type Name = {
  firstName: string;
  lastName: string;
};

type Address = {
  city: string;
};

const person: Name & Address = {
  firstName: "jon",
  lastName: "doe",
  city: "scranton",
};

Myös kannattaa huomioida, että type otettiin käyttöön myöhemmin verrattuna interface ja TS:n viimeisimmän julkaisun mukaan type voi tehdä *melkein kaiken mitä interface voi ja paljon muuta!

*paitsi Declaration merging (henkilökohtainen mielipide: On hyvä, ettei sitä tueta tyyppinä, sillä se voi johtaa koodin epäjohdonmukaisuuteen)

Vastaus #10

Indeksoinnin ero.

interface MyInterface {
  foobar: string;
}

type MyType = {
  foobar: string;
}

const exampleInterface: MyInterface = { foobar: 'hello world' };
const exampleType: MyType = { foobar: 'hello world' };

let record: Record<string, string> = {};

record = exampleType;      // Compiles
record = exampleInterface; // Index signature is missing

Asiaankuuluva ongelma: Indeksin allekirjoitus puuttuu tyypistä (vain rajapinnoissa, ei tyyppialiaaseissa)

Joten ota tämä esimerkki huomioon, jos haluat indeksoida objektisi

Katsokaa tätä kysymystä ja tätä liittyen Liskovin periaatteen rikkomiseen

Arvioinnin ero

Katso ExtendFirst-funktion tulostyyppi, kun FirstLevelType on rajapinta

/**
 * When FirstLevelType is interface 
 */

interface FirstLevelType<A, Z> {
    _: "typeCheck";
};

type TestWrapperType<T, U> = FirstLevelType<T, U>;


const a: TestWrapperType<{ cat: string }, { dog: number }> = {
  _: "typeCheck",
};

// {  cat: string; }
type ExtendFirst = typeof a extends FirstLevelType<infer T, infer _>
    ? T
    : "not extended";

Katso ExtendFirst-funktion tulostyyppi, kun FirstLevelType on tyyppi:

/**
 * When FirstLevelType is type
 */
type FirstLevelType<A, Z>= {
    _: "typeCheck";
};

type TestWrapperType<T, U> = FirstLevelType<T, U>;


const a: TestWrapperType<{ cat: string }, { dog: number }> = {
  _: "typeCheck",
};

// unknown
type ExtendFirst = typeof a extends FirstLevelType<infer T, infer _>
    ? T
    : "not extended";

Vastaus #11

Päivittäisessä kehitystyössäni käytän tätä pikalistaa, kun en tiedä, minkä valita.

Lisätietoja voit lukea blogistani: https://medium.com/@magenta2127/use-which-interface-or-type-alias-in-typescript-bdfaf2e882ae

Vastaus #12

Dokumentaatiossa korostettu keskeinen ero on se, että Interface voidaan avata uudelleen lisätäkseen uuden ominaisuuden, mutta Type alias ei voi avata uudelleen lisätäkseen uuden ominaisuuden, esimerkiksi:

Tämä on ok

interface x {
  name: string
}

interface x {
  age: number
}

tämä aiheuttaa virheen Duplicate identifier y

type y = {
  name: string
}

type y = {
  age: number
}

Siitä huolimatta sekä käyttöliittymä että tyyppialiasi ovat samanlaisia.

Aiheeseen liittyviä artikkeleita