Grensesnitt vs Typer i TypeScript
Hva er forskjellen mellom disse uttalelsene (interface vs type) i TypeScript?
interface X {
a: number
b: string
}
type X = {
a: number
b: string
};
Svar #1
2019-oppdatering
De nåværende svarene og den offisielle dokumentasjonen er utdaterte. Og for de som er nye i TypeScript, er ikke terminologien som brukes tydelig uten eksempler. Nedenfor er en liste over oppdaterte forskjeller.
1. Objekter/Funksjoner
Begge kan brukes til å beskrive formen på et objekt eller en funksjons signatur. Men syntaksen er forskjellig.
Grensesnitt
interface Point {
x: number;
y: number;
}
interface SetPoint {
(x: number, y: number): void;
}
Typealias
type Point = {
x: number;
y: number;
};
type SetPoint = (x: number, y: number) => void;
2. Andre typer
I motsetning til et grensesnitt, kan typealiaset også brukes for andre typer som primitivtyper, unioner og tupler.
// primitive
type Name = string;
// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };
// union
type PartialPoint = PartialPointX | PartialPointY;
// tuple
type Data = [number, string];
3. Utvid
Begge kan utvides, men igjen, syntaksen er forskjellig. Merk også at et grensesnitt og en typealias ikke er gjensidig utelukkende. Et grensesnitt kan utvide et typealias, og omvendt.
Grensesnitt utvider grensesnitt
interface PartialPointX { x: number; }
interface Point extends PartialPointX { y: number; }
Type-alias utvider type-alias
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };
Grensesnitt utvider typealias
type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }
Type alias utvider grensesnitt
interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; };
4. Redskaper
En klasse kan implementere et grensesnitt eller en typealias, begge på nøyaktig samme måte. Merk imidlertid at en klasse og et grensesnitt anses som statiske blåkopier. Derfor kan de ikke implementere/utvide en typealias som navngir en unionstype.
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. Deklarasjonssammenslåing
I motsetning til en typealias, kan et grensesnitt defineres flere ganger, og vil bli behandlet som et enkelt grensesnitt (med medlemmer fra alle erklæringer slått sammen).
// 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 };
Svar #2
Oppdatering mars 2021: Den nyere TypeScript-håndboken (også nevnt i nju-clc-svaret nedenfor) har et avsnitt 'Interfaces vs. Type Aliases' som forklarer forskjellene.
Original svar (2016)
I henhold til den (nå arkiverte) TypeScript-språkspesifikasjonen:
I motsetning til en grensesnittdeklarasjon, som alltid introduserer en navngitt objekttype, en typealiasdeklarasjon kan introdusere et navn for hvilken som helst type, inkludert primitive, union og intersect-typer.
Spesifikasjonen fortsetter med å nevne:
Grensesnitttyper har mange likheter med typealias for objektlitteraler, men siden grensesnitttyper tilbyr flere muligheter, foretrekkes de generelt fremfor typealias. For eksempel, grensesnitttypen
interface Point { x: number; y: number; }kan skrives som typealias
type Point = { x: number; y: number; };Men å gjøre det betyr at følgende funksjoner går tapt:
Et grensesnitt kan navngis i en extends- eller implements-setning, men en typealias for en objekt-type-literal kan ikkeIkke lenger sant siden TS 2.7.- Et grensesnitt kan ha flere sammenslåtte deklarasjoner, men en typealias for en objekt-type literal kan ikke.
Svar #3
Relevant i 2021
For TypeScript-versjon: 4.3.4
Kort fortalt;
Min personlige konvensjon, som jeg beskriver nedenfor, er denne:
Foretrekk alltid
interfacefremfortype.
Når du skal bruke type:
- Bruk
typenår du definerer et alias for primitive typer (string, boolean, number, bigint, symbol, osv.) - Bruk
typenår du definerer tuppeltyper - Bruk
typenår du definerer funksjonstyper - Bruk
typenår du definerer en union - Bruk
typenår du prøver å overbelaste funksjoner i objekttyper via komposisjon - Bruk
typenår du trenger å dra nytte av kartlagte typer
Når du skal bruke interface:
- Bruk
interfacefor alle objekttyper der bruk avtypeikke er nødvendig (se ovenfor) - Bruk
interfacenår du vil dra nytte av deklarasjonssammenslåing.
Primitive typer
Den enkleste forskjellen å se mellom type og interface er at bare type kan brukes til å gi et alias for en primitiv:
type Nullish = null | undefined;
type Fruit = 'apple' | 'pear' | 'orange';
type Num = number | bigint;
Ingen av disse eksemplene er mulig å oppnå med grensesnitt.
💡 Når du gir et typealias for en primitiv verdi, bruk type-nøkkelordet.
Tuplatyper
Tupler kan kun typiseres via type-nøkkelordet:
type row = [colOne: number, colTwo: string];
💡 Bruk type-nøkkelordet når du angir typer for tupler.
Funksjonstyper
Funksjoner kan types av både type og interface-nøkkelordene:
// via type
type Sum = (x: number, y: number) => number;
// via interface
interface Sum {
(x: number, y: number): number;
}
Siden den samme effekten kan oppnås på begge måter, vil regelen være å bruke type i disse scenarioene siden det er litt lettere å lese (og mindre omstendelig).
💡 Bruk type når du definerer funksjonstyper.
Unionstyper
Unionstyper kan bare oppnås med type-nøkkelordet:
type Fruit = 'apple' | 'pear' | 'orange';
type Vegetable = 'broccoli' | 'carrot' | 'lettuce';
// 'apple' | 'pear' | 'orange' | 'broccoli' | 'carrot' | 'lettuce';
type HealthyFoods = Fruit | Vegetable;
💡 Når du definerer unionstyper, bruk type-nøkkelordet
Objekttyper
Et objekt i JavaScript er et nøkkel/verdi-kart, og en "objekttype" er TypeScripts måte å angi typer for disse nøkkel/verdi-kartene på. Både interface og type kan brukes når man oppgir typer for et objekt, som det opprinnelige spørsmålet tydelig viser. Så når bruker man type vs interface for objekttyper?
Snitt vs Arv
Med typer og sammensetning kan jeg gjøre noe slikt:
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 er helt fornøyd. Hva med om jeg prøvde å utvide det med et grensesnitt:
interface StrAndNumLogger extends NumLogger {
log: (val: string) => void;
};
Deklarasjonen av StrAndNumLogger gir meg en feil:
Interface 'StrAndNumLogger' incorrectly extends interface 'NumLogger'Med grensesnitt må undertypene nøyaktig samsvare med typene som er erklært i supertypen, ellers vil TS kaste en feil som den ovenfor.
💡 Når du prøver å overbelaste funksjoner i objekttyper, vil du ha det bedre med å bruke type-nøkkelordet.
Deklarasjonssammenslåing
Det viktigste aspektet ved interfaces i TypeScript som skiller dem fra typer, er at de kan utvides med ny funksjonalitet etter at de allerede er erklært. Et vanlig brukstilfelle for denne funksjonen oppstår når du vil utvide typene som eksporteres fra en node-modul. For eksempel, @types/jest eksporterer typer som kan brukes når du arbeider med jest-biblioteket. Men jest tillater også å utvide hovedtypen jest med nye funksjoner. For eksempel kan jeg legge til en tilpasset test slik:
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
);
Og så kan jeg bruke det slik:
test.timedTest('this is my custom test', () => {
expect(true).toBe(true);
});
Og nå vil tiden som har gått for den testen bli skrevet ut til konsollen når testen er ferdig. Flott! Det er bare ett problem - typescript har ingen anelse om at jeg har lagt til en timedTest-funksjon, så det vil kaste en feil i editoren (koden vil kjøre fint, men TS vil være sint).
For å løse dette, må jeg fortelle TS at det finnes en ny type i tillegg til de eksisterende typene som allerede er tilgjengelige fra jest. For å gjøre det, kan jeg gjøre dette:
declare namespace jest {
interface It {
timedTest: (name: string, fn: (mockTrack: Mock) => any, timeout?: number) => void;
}
}
På grunn av hvordan grensesnitt fungerer, vil denne typedeklarasjonen bli slått sammen med typedeklarasjonene som eksporteres fra @types/jest. Så jeg har ikke bare omdefinert jest.It; jeg utvidet jest.It med en ny funksjon slik at TS nå er klar over min tilpassede testfunksjon.
Denne typen ting er ikke mulig med type-nøkkelordet. Hvis @types/jest hadde erklært typene sine med type-nøkkelordet, ville jeg ikke ha kunnet utvide disse typene med mine egne tilpassede typer, og derfor ville det ikke vært noen god måte å gjøre TS fornøyd med min nye funksjon på. Denne prosessen som er unik for interface-nøkkelordet kalles deklarasjonssammenslåing.
Deklarasjonssammenslåing er også mulig å gjøre lokalt på denne måten:
interface Person {
name: string;
}
interface Person {
age: number;
}
// no error
const person: Person = {
name: 'Mark',
age: 25
};
Hvis jeg hadde gjort nøyaktig det samme som ovenfor med type-nøkkelordet, ville jeg fått en feil siden typer ikke kan erklæres/redefineres. I den virkelige verden er JavaScript-objekter mye som dette interface-eksemplet; de kan oppdateres dynamisk med nye felter ved kjøring.
💡 Fordi grensesnittdeklarasjoner kan slås sammen, representerer grensesnitt mer nøyaktig den dynamiske naturen til JavaScript-objekter enn typer gjør, og de bør foretrekkes av den grunn.
Kartlagte objekttyper
Med type-nøkkelordet kan jeg dra nytte av kartlagte typer på denne måten:
type Fruit = 'apple' | 'orange' | 'banana';
type FruitCount = {
[key in Fruit]: number;
}
const fruits: FruitCount = {
apple: 2,
orange: 3,
banana: 4
};
Dette kan ikke gjøres med grensesnitt:
type Fruit = 'apple' | 'orange' | 'banana';
// ERROR:
interface FruitCount {
[key in Fruit]: number;
}
💡 Når du trenger å dra nytte av kartlagte typer, bruk type-nøkkelordet
Ytelse
Mye av tiden fungerer en enkel typealias til en objekttype veldig likt en grensesnitt.
interface Foo { prop: string }
type Bar = { prop: string };
Men, og så snart du trenger å komponere to eller flere typer, har du muligheten til å utvide disse typene med et grensesnitt, eller kryss-sjekke dem i et typealias, og det er da forskjellene begynner å spille en rolle.
Grensesnitt oppretter en enkelt flat objekttype som oppdager egenskapskonflikter, som vanligvis er viktige å løse! Snarere smelter snitt derimot bare egenskaper sammen rekursivt, og i noen tilfeller produserer de never. Grensesnitt vises også konsekvent bedre, mens typealias til snitt ikke kan vises i deler av andre snitt. Typeforhold mellom grensesnitt blir også bufret, i motsetning til snittyper som helhet. En siste bemerkelsesverdig forskjell er at når man sjekker mot en målsnittstype, blir hvert bestanddel sjekket før man sjekker mot den "effektive"/"flattened" typen.
Av denne grunn anbefales det å utvide typer med interfaces/extends fremfor å lage krysstyper.
Også, fra TypeScript-dokumentasjonen
Det sagt, anbefaler vi at du bruker grensesnitt fremfor typealiaser. Spesielt fordi du vil få bedre feilmeldinger. Hvis du holder musen over følgende feil, kan du se hvordan TypeScript kan gi kortere og mer fokuserte meldinger når du jobber med grensesnitt som Chicken.
Mer i TypeScript-wikien.
Svar #4
Fra og med TypeScript 3.2 (november 2018), er følgende sant:
| Aspekt | Type | Grensesnitt |
|---|---|---|
| Kan beskrive funksjoner | ✅ | ✅ |
| Kan beskrive konstruktører | ✅ | ✅ |
| Kan beskrive tuples | ✅ | ✅ |
| Grensesnitt kan utvide det | ⚠️ | ✅ |
| Klasser kan utvide det | 🚫 | ✅ |
Klasser kan implementere det (implements) | ⚠️ | ✅ |
| Kan krysse et annet av sitt slag | ✅ | ⚠️ |
| Kan lage en union med en annen av sin slags | ✅ | 🚫 |
| Kan brukes til å lage kartlagte typer | ✅ | 🚫 |
| Kan kartlegges over med kartlagte typer | ✅ | ✅ |
| Utvider i feilmeldinger og logger | ✅ | 🚫 |
| Kan forsterkes | 🚫 | ✅ |
| Kan være rekursiv | ⚠️ | ✅ |
⚠️ I noen tilfeller
Svar #5
Når bruke type?
Generelle transformasjoner
Bruk type når du transformerer flere typer til en enkelt generisk type.
Eksempel :
type Nullable<T> = T | null | undefined
type NonNull<T> = T extends (null | undefined) ? never : T
Typealiasering
Vi kan bruke type for å lage aliaser for lange eller kompliserte typer som er vanskelige å lese, samt upraktiske å skrive om og om igjen.
Eksempel :
type Primitive = number | string | boolean | null | undefined
Å lage et alias som dette gjør koden mer konsis og lesbar.
Typefangst
Bruk type for å fange typen til et objekt når typen er ukjent.
Eksempel :
const orange = { color: "Orange", vitamin: "C"}
type Fruit = typeof orange
let apple: Fruit
Her får vi den ukjente typen orange, kaller den en Fruit og bruker deretter Fruit til å lage et nytt typesikkert objekt apple.
Når bør man bruke interface?
Polymorfisme
Et interface er en kontrakt for å implementere en form på dataene. Bruk interface for å gjøre det klart at det er ment å bli implementert og brukt som en kontrakt om hvordan objektet vil bli brukt.
Eksempel :
interface Bird {
size: number
fly(): void
sleep(): void
}
class Hummingbird implements Bird { ... }
class Bellbird implements Bird { ... }
Selv om du kan bruke type for å oppnå dette, blir Typescript sett mer på som et objektorientert språk, og interface har en spesiell plass i objektorienterte språk. Det er lettere å lese koden med interface når du jobber i et teammiljø eller bidrar til åpen kildekode. Det er også lett for nye programmerere som kommer fra andre objektorienterte språk.
Den offisielle Typescript-dokumentasjonen sier også:
... vi anbefaler å bruke et
interfacefremfor entype-alias når det er mulig.
Dette antyder også at type er mer ment for å lage typealiaser enn å lage selve typene.
Erklæringssammenslåing
Du kan bruke erklæringssammenslåingsfunksjonen til interface for å legge til nye egenskaper og metoder til en allerede erklært interface. Dette er nyttig for de omgivende typeredeklarasjonene til tredjepartsbiblioteker. Når noen deklarasjoner mangler for et tredjepartsbibliotek, kan du erklære grensesnittet på nytt med samme navn og legge til nye egenskaper og metoder.
Eksempel :
Vi kan utvide den ovennevnte Bird-grensesnittet til å inkludere nye erklæringer.
interface Bird {
color: string
eat(): void
}
Det er det! Det er lettere å huske når man skal bruke hva enn å gå seg vill i subtile forskjeller mellom de to.
Svar #6
TypeScript-håndboken gir svaret:
Nesten alle funksjoner i et grensesnitt er tilgjengelige i en type.
Den viktigste forskjellen er at en type ikke kan åpnes på nytt for å legge til nye egenskaper, i motsetning til et grensesnitt som alltid kan utvides.
Svar #7
Oppdatering april 2023:
https://www.typescriptlang.org/docs/handbook/2/types-from-types.html (Gå til den nye siden)
https://www.typescriptlang.org/docs/handbook/advanced-types.html (Denne siden har blitt foreldet)
En forskjell er at grensesnitt oppretter et nytt navn som brukes overalt. Typealias oppretter ikke et nytt navn — for eksempel vil ikke feilmeldinger bruke aliasnavnet.
Svar #8
Eksempler med typer:
// lag en trestruktur for et objekt. Du kan ikke gjøre det samme med interface på grunn av mangel på intersecting (&)
type Tree<T> = T & { parent: Tree<T> };
// type for å begrense en variabel til å kun tildele noen få verdier. Interfaces har ikke union (|)
type Choise = "A" | "B" | "C";
// takket være typer, kan du erklære NonNullable-type takket være en betinget mekanisme.
type NonNullable<T> = T extends null | undefined ? never : T;
Eksempler med grensesnitt:
// du kan bruke interface for OOP og bruke 'implements' for å definere objekt-/klasseskjelett
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)
}
}
// du kan utvide grensesnitt med andre grensesnitt
interface IMyObject {
label: string,
}
interface IMyObjectWithSize extends IMyObject{
size?: number
}
Svar #9
Andre svar er flotte! Noen få andre ting som Type kan gjøre, men Interface ikke kan
Du kan bruke union i type
type Name = string | { FullName: string };
const myName = "Jon"; // works fine
const myFullName: Name = {
FullName: "Jon Doe", //also works fine
};
Iterere over unionsegenskaper i type
type Keys = "firstName" | "lastName";
type Name = {
[key in Keys]: string;
};
const myName: Name = {
firstName: "jon",
lastName: "doe",
};
Kryss i type (men også støttet i grensesnitt med extends)
type Name = {
firstName: string;
lastName: string;
};
type Address = {
city: string;
};
const person: Name & Address = {
firstName: "jon",
lastName: "doe",
city: "scranton",
};
Merk også at type ble introdusert senere sammenlignet med interface, og ifølge den nyeste utgivelsen av TS kan type gjøre *nesten alt som interface kan og mye mer!
*bortsett fra Declaration merging (personlig mening: Det er bra at det ikke støttes i type da det kan føre til uoverensstemmelser i koden)
Svar #10
Forskjell i indeksering.
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
Relatert problem: Indeks-signatur mangler i typen (kun på grensesnitt, ikke på typealias)
Så vennligst vurder dette eksemplet, hvis du vil indeksere objektet ditt
Ta en titt på dette spørsmålet og dette om brudd på Liskov-prinsippet
Forskjell i vurdering
Se resultattypen til ExtendFirst når FirstLevelType er et grensesnitt
/**
* 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";
Se resultattypen til ExtendFirst når FirstLevelType er en type:
/**
* 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";
Svar #11
I min daglige utvikling bruker jeg dette juksearket når jeg ikke vet hvilken jeg skal velge.
For mer informasjon kan du lese bloggen min: https://medium.com/@magenta2127/use-which-interface-or-type-alias-in-typescript-bdfaf2e882ae
Svar #12
Den viktigste forskjellen som påpekes i dokumentasjonen er at Interface kan åpnes på nytt for å legge til nye egenskaper, men Type alias kan ikke åpnes på nytt for å legge til nye egenskaper, for eksempel:
Dette er greit
interface x {
name: string
}
interface x {
age: number
}
dette vil kaste feilen Duplicate identifier y
type y = {
name: string
}
type y = {
age: number
}
Bortsett fra det er både grensesnitt og typealias like.
