TypeScript
有一種類型宣告方式
有時候這個模式叫做 duck typing
或是 structural subtyping
,
或統稱為 interface
最簡單的的 interface
function printLabel(labelledObj: {label: string}){
console.log(labelledObj.label);
}
let myObj = {size: 10, label: 'Size 10 Object'};
printLabel(myObj);
呼叫 printLabel
的時候會進行 type-check,而在 printLabel
中就有參數檢查
label
必須是 string
實際上可能有更多的屬性 不只是 label
檢查只會檢查 label
屬性是不是字串
有些狀況 TypeScript
並不寬鬆
依據上面的範例可以使用 interface
指定 label
為必要參數
interface LabelledValue{
label: string
}
function printLabel(labelledObj: LabelledValue){
console.log(labelledObj.label);
}
let myObj = {size: 10, label: 'Size 10 Object'};
printLabel(myObj);
LabelledValue
是我們可以描述參數必要性的範例
代表輸入值必須要有一個 label
變數型態為字串
我們並不需要非常明確的指定 printLabel
這個 Function 的輸入參數
只要符合這個 interface
就會允許使用
也可以定義不一定會存在的參數
interface Squareconfig{
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): {color: string, area: number}{
let newSquare = {color: "white", area: 100};
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({color: "black"});
有些 properties 應該只能被修改 無法整個被覆寫
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = {x: 10, y: 20};
p1.x = 5; // Error
也可以定義一個唯讀的陣列
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; //Error
ro.push(8); //Error
ro.length = 100; // Error
a = ro; //Error
最後一行中,當你定義為普通 ReadArray
要 assign 給一個 Array
是不允許的
const
只是禁止你的物件被覆寫
而 readonly
則是設定你的物件中的參數被覆寫
在第一個範例中,雖然我們寫了一個 interface
是 {size: number, label: string}
但是我們真正有使用的只有 {label: string}
我們在剛剛也有提到 optional properties
或是稱為 option bags
但是這兩個一起使用的話也有可能產生一些問題
interface SquareConfig{
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): {color: string, area: number}{
//...
}
let mySquare = createSquare({colour: "red", width: 100});
上述範例中 creteSquare
中的 colour
拼錯了
正確應該是 color
, 並且 TypeScript
會顯示編譯錯誤
然而你可以辯解說因為 width
是正確的
color
並不存在,但是 colour
名稱的錯誤是微不足道的
// error: 'colour' not expected in type 'SquareConfig'
let mySquare = createSquare({ colour: "red", width: 100 });
這時候正規的實作方式可以是
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
我們將會討論 index signatures
但是在這裡可以說 SquareConfig
可以有任意數量的 properties
不論是不是 color
或是 width
他們並不在意
另外還有一種方法 你直接宣告一個 SquareOptions
物件來放入 createSquare
中也不會有錯誤出現
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
請記得上述的範例 不應該讓這些檢查類別變得更加的複雜
你應該要持續檢查這些類型,因為大多數的錯誤都會造成 bugs。如果你允許 在 createSquare
中使用 color
或是 colour
這兩個參數
你應該修改 squareConfig
來顯示這兩種使用情境
interfaces
可以用來描述物件的輪廓
然而為了要可以描述物件的 properties
所以 interfaces
應該也是可以描述 Function types
interfaces
描述一個 function type
的時候只需要定義 parameter 列表和回傳值
每一個 parameter 都需要明確的定義名稱和類別
interface SearchFunc{
(source: string, subString: string): boolean;
}
只需要定義一次之後就可以拿這個 interface
來建立變數
interface SearchFunc{
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string){
let result = source.search(subString);
return result > -1;
}
在宣告 Function
的時候 parameter 的名字不一定要一樣
interface SearchFunc{
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean{
let result = src.search(sub);
return result > -1;
}
宣告也可以只宣告一次
之後依據同類型宣告的 Function
也會依照之前宣告的 interface
做檢查,不避在重複定義。
interface SearchFunc{
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
return result > -1;
}
基本上我們可以用 interface
來定義 Function
也可以來定義 index
interface StringArray{
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
在上方的範例中 StringArray
中有宣告一個 index type 為 number
。
基本上只有 兩種類型的 index
, 就是 number
和 string
也可以同時支援兩種類別,但是在支援兩種類別的時候若是為 100
則必須是回傳 '100'
也就是兩種類別必須要統一
class Animal{
name: string;
}
class Dog extends Animal{
breed: string;
}
interface NotOkay{
[x: number]: Animal;
[x: string]: Dog;
}
string
是非常實用的宣告 index
方式
因為 obj.property
也可以視為 obj['property']
這一個範例因為 name
的類別並不匹配,所以在檢查類別的時候會有錯誤
interface NumberDictionary{
[index: string]: number;
length: number; //ok, length is a number
name: string; //error name is not a subtype of the indexer
}
最後我們試著宣告一個唯讀的 interface
interface ReadonlyStringArray{
readonly [index: number]: string
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // Error
interface ClockInterface{
currentTime: Date;
}
class Clock implements ClockInterface{
currentTime: Date;
constructor(h: number, m: number){}
}
也可以描述在 class 中的 method
例如在 Clock
中描述一個 setTime
的 method
interface ClockInterface{
currentTime: Date;
setTime(d: Date): void;
}
class Clock implements ClockInterface{
currentTime: Date;
setTime(d: Date){
this.currentTime = d;
}
constructor(h: number, m: number){}
}
當我們要使用 interface
來宣告 class
的時候
要記得 class
有兩種類型,一種是 public 一種是 static 當你要宣告一個 class
的 constructor 的時候會有錯誤
interface ClockConstructor{
new (hour: number, minute: number);
}
class Clock implements ClockConstructor{
currentTime: Date;
constructor(h: number, m: number) { }
}
這是因為當一個 class
轉為 instance
的時候
只有 instance
這邊有做 typing-check 而再 static-side 並沒有包含這個檢查
所以在下面的這個範例,需要定義兩個 interface
ClockContructor
是為了 constructor 而 ClockInterface
是為了實體化後的物件定義
而會了方便我們定義 constructor 所以又建立一個 createClock
來做這件事情
interface ClockConstructor {
new (hour: number, minute: number);
}
interface ClockInterface {
tick();
}
function createClock(
ctor: ClockConstructor,
hour: number,
minute: number
): ClockInterface {
return new ctor(hour, minute);
}
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("beep beep");
}
}
class AnalogClock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("tick tock");
}
}
let digital = createClock(DigitalClock, 12, 18);
let analog = createClock(AnalogClock, 7, 21);
因為 createClock
的第一個參數是 ClockConstructor
在 createClock(AnalogClock, 7, 21)
中檢查 Analogclock
的 constructor 是否有正確的參數類型
就像 classes
一樣 interface
可以利用繼承將他們的屬性傳給自己的 Children
interface Shape {
clolor: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = <Square>{};
square.color = 'blue';
square.sideLength = 10;
也允許多重繼承,建立一個集合體
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
let s = <Square>{};
s.color = "blue";
s.sideLength = 10;
s.penWidth = 5.0;
Javascript 常常會有很豐富的一個 多次繼承,也可以使用 Hybird Type
來做多個繼承
interface Conter{
(start: number): string;
interval: number;
reset():void;
}
function getCounter():Counter{
let counter = <Counter>function(start: number){};
counter.interval = 123;
counter.reset = function(){};
;return counter;
}
let c = getCounter();
c(10);
c.reset()
c.interval = 5.0;
當一個 interface
繼承了一個 class 只是繼承了他的屬性而不是他的實體只是繼承了他的屬性而不是他的實體
這就是說當你要實踐這個 interface
的同時也必需繼承同一個 class 來實現他的所有屬性
當你有一個很大的繼承架構
但是又想要自訂一個程式碼專為某一個 subclass 中的某些屬性 又不希望她繼承所有的父輩繼承
class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}
class Button extends Control implements SelectableControl {
select() { }
}
class TextBox extends Control {
}
// Error: Property 'state' is missing in type 'Image'.
class Image implements SelectableControl {
state = 1;
select() { }
}
class Location {
}
上述範例中 SelectableControl
包含了所有的 Control
的屬性
包含 private
的 state,這意味著之後要實現 SelectableControl
的同時只能 extends Control
一個類別去承接他的 private
的 state
在 Control
之中允許透過 SelectableControl
來取得 private
state
而 SelectableControl
就像是 Control
知道他還會有一個 function select
Button
和 TextBox
是 SelectableContorl
的子類
因為他們都是繼承 Control
但是 Image
和 Location
則不是