TypeScript Interface vs Type知多少
作者:陽(yáng)呀呀
來(lái)源:SegmentFault
接口和類型別名非常相似,在大多情況下二者可以互換。在寫TS的時(shí)候,想必大家都問(wèn)過(guò)自己這個(gè)問(wèn)題,我到底應(yīng)該用哪個(gè)呢?希望看完本文會(huì)給你一個(gè)答案。知道什么時(shí)候應(yīng)該用哪個(gè),首先應(yīng)該了解二者之間的相同點(diǎn)和不同點(diǎn),再做出選擇。
接口 vs 類型別名 相同點(diǎn)
1. 都可以用來(lái)描述對(duì)象或函數(shù)
interface Point {
x: number
y: number
}
interface SetPoint {
(x: number, y: number): void;
}
type Point = {
x: number;
y: number;
};
type SetPoint = (x: number, y: number) => void;
2. 都可以擴(kuò)展
兩者的擴(kuò)展方式不同,但并不互斥。接口可以擴(kuò)展類型別名,同理,類型別名也可以擴(kuò)展接口。
接口的擴(kuò)展就是繼承,通過(guò) extends 來(lái)實(shí)現(xiàn)。類型別名的擴(kuò)展就是交叉類型,通過(guò) & 來(lái)實(shí)現(xiàn)。
// 接口擴(kuò)展接口
interface PointX {
x: number
}
interface Point extends PointX {
y: number
}
// 類型別名擴(kuò)展類型別名
type PointX = {
x: number
}
type Point = PointX & {
y: number
}
// 接口擴(kuò)展類型別名
type PointX = {
x: number
}
interface Point extends PointX {
y: number
}
// 類型別名擴(kuò)展接口
interface PointX {
x: number
}
type Point = PointX & {
y: number
}
接口 vs 類型別名不同點(diǎn)
1. 類型別名更通用(接口只能聲明對(duì)象,不能重命名基本類型)
類型別名的右邊可以是任何類型,包括基本類型、元祖、類型表達(dá)式(&或|等類型運(yùn)算符);而在接口聲明中,右邊必須為結(jié)構(gòu)。例如,下面的類型別名就不能轉(zhuǎn)換成接口:
type A = number
type B = A | string
2. 擴(kuò)展時(shí)表現(xiàn)不同
擴(kuò)展接口時(shí),TS將檢查擴(kuò)展的接口是否可以賦值給被擴(kuò)展的接口。舉例如下:
interface A {
good(x: number): string,
bad(x: number): string
}
interface B extends A {
good(x: string | number) : string,
bad(x: number): number // Interface 'B' incorrectly extends interface 'A'.
// Types of property 'bad' are incompatible.
// Type '(x: number) => number' is not assignable to type '(x: number) => string'.
// Type 'number' is not assignable to type 'string'.
}
但使用交集類型時(shí)則不會(huì)出現(xiàn)這種情況。我們將上述代碼中的接口改寫成類型別名,把 extends 換成交集運(yùn)算符 &,TS將盡其所能把擴(kuò)展和被擴(kuò)展的類型組合在一起,而不會(huì)拋出編譯時(shí)錯(cuò)誤。
type A = {
good(x: number): string,
bad(x: number): string
}
type B = A & {
good(x: string | number) : string,
bad(x: number): number
}
3. 多次定義時(shí)表現(xiàn)不同
接口可以定義多次,多次的聲明會(huì)合并。但是類型別名如果定義多次,會(huì)報(bào)錯(cuò)。
interface Point {
x: number
}
interface Point {
y: number
}
const point: Point = {x:1} // Property 'y' is missing in type '{ x: number; }' but required in type 'Point'.
const point: Point = {x:1, y:1} // 正確
type Point = {
x: number // Duplicate identifier 'A'.
}
type Point = {
y: number // Duplicate identifier 'A'.
}
到底應(yīng)該用哪個(gè)
如果接口和類型別名都能滿足的情況下,到底應(yīng)該用哪個(gè)是我們關(guān)心的問(wèn)題。感覺(jué)哪個(gè)都可以,但是強(qiáng)烈建議大家只要能用接口實(shí)現(xiàn)的就優(yōu)先使用接口,接口滿足不了的再用類型別名。
為什么會(huì)這么建議呢?其實(shí)在TS的wiki中有說(shuō)明。具體的文章地址在這里。
以下是Preferring Interfaces Over Intersections的譯文:
大多數(shù)時(shí)候,對(duì)于聲明一個(gè)對(duì)象,類型別名和接口表現(xiàn)的很相似。
interface Foo { prop: string }
type Bar = { prop: string };然而,當(dāng)你需要通過(guò)組合兩個(gè)或者兩個(gè)以上的類型實(shí)現(xiàn)其他類型時(shí),可以選擇使用接口來(lái)擴(kuò)展類型,也可以通過(guò)交叉類型(使用
&創(chuàng)造出來(lái)的類型)來(lái)完成,這就是二者開(kāi)始有區(qū)別的時(shí)候了。
接口會(huì)創(chuàng)建一個(gè)單一扁平對(duì)象類型來(lái)檢測(cè)屬性沖突,當(dāng)有屬性沖突時(shí)會(huì)提示,而交叉類型只是遞歸的進(jìn)行屬性合并,在某種情況下可能產(chǎn)生 never類型接口通常表現(xiàn)的更好,而交叉類型做為其他交叉類型的一部分時(shí),直觀上表現(xiàn)不出來(lái),還是會(huì)認(rèn)為是不同基本類型的組合 接口之間的繼承關(guān)系會(huì)緩存,而交叉類型會(huì)被看成組合起來(lái)的一個(gè)整體 在檢查一個(gè)目標(biāo)交叉類型時(shí),在檢查到目標(biāo)類型之前會(huì)先檢查每一個(gè)組分
上述的幾個(gè)區(qū)別從字面上理解還是有些繞,下面通過(guò)具體的列子來(lái)說(shuō)明。
interface Point1 {
x: number
}
interface Point extends Point1 {
x: string // Interface 'Point' incorrectly extends interface 'Point1'.
// Types of property 'x' are incompatible.
// Type 'string' is not assignable to type 'number'.
}
type Point1 = {
x: number
}
type Point2 = {
x: string
}
type Point = Point1 & Point2 // 這時(shí)的Point是一個(gè)'number & string'類型,也就是never
從上述代碼可以看出,接口繼承同名屬性不滿足定義會(huì)報(bào)錯(cuò),而相交類型就是簡(jiǎn)單的合并,最后產(chǎn)生了 number & string 類型,可以解釋譯文中的第一點(diǎn)不同,其實(shí)也就是我們?cè)诓煌c(diǎn)模塊中介紹的擴(kuò)展時(shí)表現(xiàn)不同。
再來(lái)看下面例子:
interface PointX {
x: number
}
interface PointY {
y: number
}
interface PointZ {
z: number
}
interface PointXY extends PointX, PointY {
}
interface Point extends PointXY, PointZ {
}
const point: Point = {x: 1, y: 1} // Property 'z' is missing in type '{ x: number; y: number; }' but required in type 'Point'
type PointX = {
x: number
}
type PointY = {
y: number
}
type PointZ = {
z: number
}
type PointXY = PointX & PointY
type Point = PointXY & PointZ
const point: Point = {x: 1, y: 1} // Type '{ x: number; y: number; }' is not assignable to type 'Point'.
// Property 'z' is missing in type '{ x: number; y: number; }' but required in type 'Point3'.
從報(bào)錯(cuò)中可以看出,當(dāng)使用接口時(shí),報(bào)錯(cuò)會(huì)準(zhǔn)確定位到Point。
但是使用交叉類型時(shí),雖然我們的 Point 交叉類型是 PointXY & PointZ, 但是在報(bào)錯(cuò)的時(shí)候定位并不在 Point 中,而是在 Point3 中,即使我們的 Point 類型并沒(méi)有直接引用 Point3 類型。
如果我們把鼠標(biāo)放在交叉類型 Point 類型上,提示的也是 type Point = PointX & PointY & PointZ,而不是 PointXY & PointZ。
這個(gè)例子可以同時(shí)解釋譯文中第二個(gè)和最后一個(gè)不同點(diǎn)。
結(jié)論
有的同學(xué)可能會(huì)問(wèn),如果我不需要組合只是單純的定義類型的時(shí)候,是不是就可以隨便用了。但是為了代碼的可擴(kuò)展性,建議還是優(yōu)先使用接口。現(xiàn)在不需要,誰(shuí)能知道后續(xù)需不需要呢?所以,讓我們大膽的使用接口吧~

