釐清 Type Assertion 觀念與盲點

C# 有所謂的 Object Initializer,讓我們可以很優雅的建立物件,並且將物件的 field 一次填滿,TypeScript 是否也提供如 C# 一樣的寫法呢?

Version


TypeScript 2.2

傳統建立物件方式


hero.ts

1
2
3
4
export class Hero {
name: string;
state: string;
}

使用 class 建立 model。

app.component.ts

1
2
3
4
5
6
onAddHeroClick() {
const hero = new Hero();
hero.name = 'Sam';
hero.state = 'active';
this.heroes.push(hero);
}

建立 Hero 物件,然後指定 field 值,最後再 push 進陣列。

這樣寫當然沒有錯,只是必須先透過 constructor 建立物件,然後一一指定 field 值,程式行數會很多,且沒那麼優雅。

C Sharp


1
2
3
4
5
var hero = new Hero
{
Name = "Sam",
State = "active"
};

C# 的 Object Initializer 允許我們不用透過 constructor,直接在型別後面使用 {},且 Intellisense 會自動對 field 加以提示。

這種方式比起傳統物件導向寫法優雅。

TypeScript


在 2015 年,就有人開始討論如何在 TypeScript 提供 Object Initializer,也提出各種語法上的建議。

[New Feature] Initialize Classes by Using an Object Initializer#3895

TypeScript 有 3 種寫法,可以寫出類似 C# Object Initializer 風格的程式碼。

1
2
3
4
const hero: Hero = {
name: 'Sam',
state: 'active'
};

這種寫法也不用透過 new 與 constructor,語法精簡,且 Intellisense 會自動對 field 加以提示。

1
2
3
4
const hero = <Hero>{
name: 'Sam',
state: 'active'
};

這種寫法也不用透過 new 與 constructor,使用 type assertion,類似泛型的寫法,將 object type 轉成 Hero,語法精簡,且 Intellisense 會自動對 field 加以提示。

1
2
3
4
const hero = {
name: 'Sam',
state: 'active'
} as Hero;

這種寫法也不用透過 new 與 constructor,是 type assertion 的另一種寫法,在最後補上 as 將 object type 轉成 Hero,語法精簡,且 Intellisense 會自動對 field 加以提示。

<Foo> vs. as Foo


<Foo>as Foo 寫法都屬於 type assertion,該用哪一種寫法呢?

一開始 TypeScript 只提供 <Foo>的語法,但這種寫法搭配 JSX 會有問題。

1
2
var foo = <string>bar;
</string>

因此 TypeScript 另外提供 as Foo 寫法給 JSX。

1
2
var foo = bar as string;
</string>

對於 Angular 來說,<Foo>as Foo 都可以用,但就語意而言,<Foo> 寫法較優。

因為 type assertion 是在編譯時期靜態轉型,而非執行時期動態轉型,使用泛型的 <> 符號較能彰顯其編譯時期的特性。

Type Assertion vs. Type Casting


或許你會覺得 type assertion 就是一種轉型而已,只是使用了 <Foo>as Foo 的語法,但事實上並不是如此。

在 TypeScript PlayGround,我們可以發現使用 type assertion 之後的 JavaScript 的差異:

TypeScript

1
2
3
4
const hero = <Hero>{
name: 'Sam',
state: 'active'
};

JavaScript

1
2
3
4
var hero = {
name: 'Sam',
state: 'active'
};

也就是對於 JavaScript 而言,並沒有所謂的 Hero 型別,只是 object 型別,所以並沒有所謂的執行時期動態轉型,type assertion 實際上只有兩個功能:

  • 編譯時期的型別檢查
  • 開發時期的 Intellisense。

Type Assertion 的盲點


Type assertion 並非萬靈丹,事實上它有以下盲點,回顧一下我們的 model:

hero.ts

1
2
3
4
export class Hero {
name: string;
state: string;
}

Hero 有兩個 fields。

assert000

使用 <Hero> 寫法,就算少寫了 state,TypeScript language service 與 TSLint 都不會警告,且編譯後也沒有錯誤。

assert001

改用了 as Hero 寫法依舊,就算少寫了 state,TypeScript language service 與 TSLint 都不會警告,且編譯後也沒有錯誤。

assert002

若使用了型別宣告,少寫了 state,TypeScript language service 會提出警告,編譯也會失敗,明確告知少指定了 state fields。

以上 3 種寫法,Intellisense 皆正常,但只有明確宣告型別,才能完整檢查出少了 field。

Nullable


若 model 有些 field 允許不指定值,卻又希望 TypeScript 強型別檢查呢?

1
2
3
4
export class Hero {
name: string;
state?: string;
}

請在 field 名稱後方加上 ?,則 TypeScript language service 不會提出警告,編譯也不會失敗。

Conclusion


  • Type Assertion 並非最完美的強型別解決方案,只能對 Intellisense 有幫助。
  • 若要完整的檢查,還是要明確的指定型別,如此才能發揮 TypeScript 的 type 威力。

Sample Code


完整的範例可以在我的 GitHub 上找到。

Reference


TypeScript, [New Feature] Initialize Classes by Using an Object Initializer#3895
TypeScript Deep Dive, Type Assertion

2017-05-22