typescript(以下簡稱TS)出來也有好長時間了,下面記錄一下學習心得。
首先學這門語言前,請確保有以下基礎知識:
- 紮實的javascript基礎知識
- es6的基礎知識
- 物件導向程式設計的概念(沒有也可以,就當是重新學一遍了)
接下來看一下TS的一些概念:
一、基本型別
TS的基礎型別有:字串(string)、數字(number)、布林值(boolean)、空(null)、未定義(undefined)、陣列(array)、物件(object)、元組(tuple)、列舉(enum)、any、void、never等12種。
寫法為在變數後加冒號然後跟變數型別的方式,例如:
1.字串
寫法:
let str: string = 'str';
2.數字
寫法:
let num: number = 123;
3.布林值
寫法:
let bol: boolean = false;
4.null
寫法:
let n: null = null;
5.undefined
寫法:
let u: undefined = undefined;
6.陣列
寫法:
let arr: number[] = [1,23,4,]; let arr1: Array<number> = [1,2,3];// 使用泛型的方式宣告變數
7.物件
寫法:
let obj: object={};
8.元組
寫法:
let tuple: [number,string] = [12,'3'];
9.列舉
寫法:
enum Num{ one=1,// 從幾開始,預設為從0開始 two,// 2 three// 3 };
10.any
寫法:
let notSure: any = 4; notSure = "maybe a string instead"; notSure = false; let anyArr: any = [1,2,'4',false,null];
11.viod
寫法:
function warnUser(): void { console.log("This is my warning message"); } let unusable: void = undefined; let unuse: void;
12.never
寫法:
function error(message: string): never { throw new Error(message); } // 推斷的返回值型別為never function fail() { return error("Something failed"); } // 返回never的函式必須存在無法達到的終點 function infiniteLoop(): never { while (true) { } }
PS:型別斷言:如果你很清楚一個變數比它現有型別更確切的型別,那麼你可以使用型別斷言。
型別斷言有兩種形式:
1.尖括號寫法:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
2.As寫法:
let someValueTwo: any = "this is a string";
let strLengthTwo: number = (someValueTwo as string).length;
當在TypeScript裡使用JSX時,只能使用As語法斷言。
2、介面
TypeScript的核心原則之一是對值所具有的結構進行型別檢查。 它有時被稱做“鴨式辨型法”或“結構性子型別化”。 在TypeScript裡,介面的作用就是為這些型別命名定義契約。
寫法:
interface 介面名 { attribute: type }
示例:
interface LabelledValue { label: string; } function printLabel(labelledObj: LabelledValue) { console.log(labelledObj.label); }
1.可選屬性
interface SquareConfig { color?: string; width?: number; }
2.只讀屬性
interface Point {
readonly x: number;
readonly y: number;
}
3.只讀陣列
let a: number[] = [1, 2, 3, 4]; let ro: ReadonlyArray<number> = a; ro[0] = 12; // error! ro.push(5); // error! ro.length = 100; // error! a = ro; // error! // a = ro as number[]; 用斷言修改陣列為可修改!
4.跳過額外的屬性檢查
interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): { color: string; area: number } { return { color: 'blue', area:23 } // ... } (方法1) let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig); (方法2)索引簽名 interface SquareConfig { color?: string; width?: number; [propName: string]: any; } (方法3)將這個物件賦值給一個另一個變數: 因為squareOptions不會經過額外屬性檢查 let squareOptions = { colour: "red", width: 100 }; let mySquare = createSquare(squareOptions);
5.通過介面定義函式型別
interface SearchFunc { (source: string, subString: string): boolean; } let mySearch: SearchFunc; mySearch = function(source: string, subString: string) { let result = source.search(subString); return result > -1; } // or // mySearch = function(src: string, sub: string): boolean { // let result = src.search(sub); // return result > -1; // }
6.可索引的型別
TypeScript支援兩種索引簽名:字串和數字。
可以同時使用兩種型別的索引,但是數字索引的返回值必須是字串索引返回值型別的子型別。 這是因為當使用number來索引時,JavaScript會將它轉換成string然後再去索引物件。 也就是說用 100(一個number)去索引等同於使用"100"(一個string)去索引,因此兩者需要保持一致。
interface StringArray { [index: number]: string; } let myArray: StringArray; myArray = ["Bob", "Fred"]; let myStr: string = myArray[0]; // 定義的StringArray介面,它具有索引簽名,表示當用number去索引StringArray時會得到string型別的返回值。 interface NumberDictionary { [index: string]: number; length: number; // 可以,length是number型別 name: string // 錯誤,`name`的型別與索引型別返回值的型別不匹配 } // 將索引簽名設定為只讀 interface ReadonlyStringArray { readonly [index: number]: string; } let myArray: ReadonlyStringArray = ["Alice", "Bob"]; myArray[2] = "Mallory"; // error!
7.實現介面
TypeScript也能夠用它來明確的強制一個類去符合某種契約
interface ClockInterface { currentTime: Date; setTime(d: Date); } class Clock implements ClockInterface { currentTime: Date; setTime(d: Date) { this.currentTime = d; } constructor(h: number, m: number) { } }
類靜態部分與例項部分的區別
當你操作類和介面的時候,你要知道類是具有兩個型別的:靜態部分的型別和例項的型別。 你會注意到,當你用構造器簽名去定義一個介面並試圖定義一個類去實現這個介面時會得到一個錯誤:
interface ClockConstructor { new (hour: number, minute: number); } class Clock implements ClockConstructor { currentTime: Date; constructor(h: number, m: number) { } } // 這裡因為當一個類實現了一個介面時,只對其例項部分進行型別檢查。 constructor存在於類的靜態部分,所以不在檢查的範圍內。 // 因此,我們應該直接操作類的靜態部分。 看下面的例子,我們定義了兩個介面, ClockConstructor為建構函式所用和ClockInterface為例項方法所用。 為了方便我們定義一個建構函式 createClock,它用傳入的型別建立例項。 interface ClockConstructor { new (hour: number, minute: number): ClockInterface; } 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, 17); let analog = createClock(AnalogClock, 7, 32); // 因為createClock的第一個引數是ClockConstructor型別,在createClock(AnalogClock, 7, 32)裡,會檢查AnalogClock是否符合建構函式簽名。
8.繼承介面
和類一樣,介面也可以相互繼承。 這讓我們能夠從一個介面裡複製成員到另一個介面裡,可以更靈活地將介面分割到可重用的模組裡。
interface Shape { color: 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 square = <Square>{}; square.color = "blue"; square.sideLength = 10; square.penWidth = 5.0;
9.混合型別
一個物件可以同時做為函式和物件使用,並帶有額外的屬性。
interface Counter { (start: number): string; interval: number; reset(): void; } function getCounter(): Counter { let counter = <Counter>function (start: number) { console.log(start) }; counter.interval = 123; counter.reset = function () { }; return counter; } let c = getCounter(); c(10); c.reset(); c.interval = 5.0;
10.介面繼承類
當介面繼承了一個類型別時,它會繼承類的成員但不包括其實現。就好像介面宣告瞭所有類中存在的成員,但並沒有提供具體實現一樣。 介面同樣會繼承到類的private和protected成員。 這意味著當你建立了一個介面繼承了一個擁有私有或受保護的成員的類時,這個介面型別只能被這個類或其子類所實現(implement)。
當你有一個龐大的繼承結構時這很有用,但要指出的是你的程式碼只在子類擁有特定屬性時起作用。 這個子類除了繼承至基類外與基類沒有任何關係。 例:
class Control { private state: any; } interface SelectableControl extends Control { select(): void; } class Button extends Control implements SelectableControl { select() { } } class TextBox extends Control { select() { } } // 錯誤:“Image”型別缺少“state”屬性。 class Image implements SelectableControl { select() { } } class Location { } // 在上面的例子裡,SelectableControl包含了Control的所有成員,包括私有成員state。 因為state是私有成員,所以只能夠是Control的子類們才能實現SelectableControl介面。
因為只有 Control的子類才能夠擁有一個宣告於Control的私有成員state,這對私有成員的相容性是必需的。 // 在Control類內部,是允許通過SelectableControl的例項來訪問私有成員state的。 實際上, SelectableControl介面和擁有select方法的Control類是一樣的。
Button和TextBox類是SelectableControl的子類(因為它們都繼承自Control並有select方法),但Image和Location類並不是這樣的。
3.TS類
從ECMAScript 2015,也就是ES 6開始,JavaScript程式設計師將能夠使用基於類的物件導向的方式。
1.類宣告
class CreateClass { greeting: string; constructor(message: string) { this.greeting = message; } greet() { return "Hello, " + this.greeting; } } let greeter = new CreateClass('demo');
2.類繼承
2.1類繼承:類從基類中繼承了屬性和方法。這裡,Dog是一個派生類,它派生自ParentClass基類,通過extends關鍵字。派生類通常被稱作子類,基類通常被稱作超類。
class ParentClass { move(distanceInMeters: number = 0) { console.log(`Animal moved ${distanceInMeters}m.`); } } class Dog extends ParentClass { bark() { console.log('Woof! Woof!'); } } const dog = new Dog(); dog.bark(); dog.move(10); dog.bark();
2.2類私有屬性:
class Animal { private name: string; constructor(theName: string) { this.name = theName; } } class Rhino extends Animal { constructor() { super("Rhino"); } } class Employee1 { private name: string; constructor(theName: string) { this.name = theName; } } let animal = new Animal("Goat"); let rhino = new Rhino(); let employee = new Employee1("Bob"); console.log(animal.name); // 錯誤
2.3類受保護屬性:
class Person { protected name: string; constructor(name: string) { this.name = name; } } class Employee extends Person { private department: string; constructor(name: string, department: string) { super(name) this.department = department; } public getElevatorPitch() { return `Hello, my name is ${this.name} and I work in ${this.department}.`; } } let howard = new Employee("Howard", "Sales"); console.log(howard.getElevatorPitch()); console.log(howard.name); // 錯誤
建構函式也可以被標記成 protected。 這意味著這個類不能在包含它的類外被例項化,但是能被繼承。比如:
class Person2 { protected name: string; protected constructor(theName: string) { this.name = theName; } } // Employee 能夠繼承 Person class Employee2 extends Person { private department: string; constructor(name: string, department: string) { super(name); this.department = department; } public getElevatorPitch() { return `Hello, my name is ${this.name} and I work in ${this.department}.`; } } let howard2 = new Employee2("Howard", "Sales"); let john = new Person2("John"); // 錯誤: 'Person' 的建構函式是被保護的.
2.4靜態屬性:類的靜態成員,這些屬性存在於類本身上面而不是類的例項上。
class Grid { static origin = {x: 0, y: 0}; calculateDistanceFromOrigin(point: {x: number; y: number;}) { let xDist = (point.x - Grid.origin.x); let yDist = (point.y - Grid.origin.y); return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale; } constructor (public scale: number) { } } let grid1 = new Grid(1.0); // 1x scale let grid2 = new Grid(5.0); // 5x scale console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10})); console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
2.5抽象類:抽象類做為其它派生類的基類使用。 它們一般不會直接被例項化。
abstract class AbstractClass { abstract makeSound(): void; move(): void { console.log('roaming the earch...'); } }
抽象類中的抽象方法不包含具體實現且必須在派生類中實現。 抽象方法的語法與介面方法相似。 兩者都是定義方法簽名但不包含方法體。 然而,抽象方法必須包含 abstract關鍵字並且可以包含訪問修飾符。
abstract class Department { constructor(public name: string) { } printName(): void { console.log('Department name: ' + this.name); } abstract printMeeting(): void; // 必須在派生類中實現 } class AccountingDepartment extends Department { constructor() { super('Accounting and Auditing'); // 在派生類的建構函式中必須呼叫 super() } printMeeting(): void { console.log('The Accounting Department meets each Monday at 10am.'); } generateReports(): void { console.log('Generating accounting reports...'); } } let department: Department; // 允許建立一個對抽象型別的引用 department = new Department(); // 錯誤: 不能建立一個抽象類的例項 department = new AccountingDepartment(); // 允許對一個抽象子類進行例項化和賦值 department.printName(); department.printMeeting(); department.generateReports(); // 錯誤: 方法在宣告的抽象類中不存在
2.6類當做介面使用
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};
4.泛型(generic)
function identity1<T>(arg: T): T { return arg; }
1.泛型型別:與非泛型函式的型別沒什麼不同,只是有一個型別引數在最前面,像函式宣告一樣:
function identity<T>(arg: T): T { return arg; } let myIdentity: <T>(arg: T) => T = identity; // or let myIdentity1: {<T>(arg: T): T} = identity; // 這引導我們去寫第一個泛型介面了。 我們把上面例子裡的物件字面量拿出來做為一個介面 interface GenericIdentityFn { <T>(arg: T): T; } function identity<T>(arg: T): T { return arg; } let myIdentity: GenericIdentityFn = identity;
2.泛型類
class GenericNumber<T> { zeroValue: T; add: (x: T, y: T) => T; } let myGenericNumber = new GenericNumber<number>(); myGenericNumber.zeroValue = 0; myGenericNumber.add = function(x, y) { return x + y; }; // GenericNumber類的使用是十分直觀的,並且你可能已經注意到了,沒有什麼去限制它只能使用number型別。 也可以使用字串或其它更復雜的型別。 let stringNumeric = new GenericNumber<string>(); stringNumeric.zeroValue = ""; stringNumeric.add = function(x, y) { return x + y; }; console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
5.列舉
1.數字列舉
enum Direction { Up = 1, // 使用初始值,遞增,否則預設從0開始 Down, Left, Right }
2.字串列舉
enum Direction2 { Up = "UP", // 每個字串列舉成員必須進行初始化 Down = "DOWN", Left = "LEFT", Right = "RIGHT", }
3.異構列舉(可以混合字串和數字成員)
例1:
enum BooleanLikeHeterogeneousEnum { No = 0, Yes = "YES", }
例2:
enum E { Foo, Bar, } function f(x: E) { if (x !== E.Foo || x !== E.Bar) { // ~~~~~~~~~~~ // Error! Operator '!==' cannot be applied to types 'E.Foo' and 'E.Bar'. } }
4.聯合列舉與列舉成員的型別
enum ShapeKind { Circle, Square, } interface Circle { kind: ShapeKind.Circle; radius: number; } interface Square { kind: ShapeKind.Square; sideLength: number; } let c11: Circle = { kind: ShapeKind.Square, // 正確的為ShapeKind.Circle // ~~~~~~~~~~~~~~~~ Error! radius: 100, }
5.執行時列舉
列舉是在執行時真正存在的物件
enum E { X, Y, Z } function f(obj: { X: number }) { console.log('X',obj.X); return obj.X; } // Works, since 'E' has a property named 'X' which is a number. f(E);
6.反向對映
除了建立一個以屬性名做為物件成員的物件之外,數字列舉成員還具有了反向對映
enum Enum { A } let a = Enum.A; let nameOfA = Enum[a]; // "A" // 生成的程式碼中,列舉型別被編譯成一個物件,它包含了正向對映( name -> value)和反向對映( value -> name)。 引用列舉成員總會生成為對屬性訪問並且永遠也不會內聯程式碼。 // 要注意的是 不會為字串列舉成員生成反向對映,因為列舉成員不能具有數值名,所以數字列舉成員具有反射
7.常量(const)列舉
為了避免在額外生成的程式碼上的開銷和額外的非直接的對列舉成員的訪問,我們可以使用 const列舉。 常量列舉通過在列舉上使用 const修飾符來定義。
常量列舉注意點:
1.不會生成反向對映
2.不能直接訪問值
const enum Order {
A,
B,
C,
}
8.外部列舉
外部列舉用來描述已經存在的列舉型別的形狀,簡單理解就是方便使用者編寫函式時的提示
declare enum Enum { A = 1, B, C = 2 }
外部列舉和非外部列舉之間有一個重要的區別,在正常的列舉裡,沒有初始化方法的成員被當成常數成員。 對於非常數的外部列舉而言,沒有初始化方法時被當做需要經過計算的。
用來描述一個應該存在的列舉型別的,而不是已經存在的,它的值在編譯時不存在,只有等到執行時才知道。
6.模組
TypeScript 1.5裡術語名已經發生了變化。 “內部模組”現在稱做“名稱空間”。 “外部模組”現在則簡稱為“模組”,這是為了與 ECMAScript 2015裡的術語保持一致,(也就是說 module X { 相當於現在推薦的寫法 namespace X {)。
1.匯出
變數,函式,類,型別別名或介面都可以通過export匯出
匯出宣告
export interface StringValidator { isAcceptable(s: string): boolean; } export const numberRegexp = /^[0-9]+$/; export class ZipCodeValidator implements StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); } }
匯出語句
class ZipCodeValidator implements StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); } } export { ZipCodeValidator }; export { ZipCodeValidator as mainValidator };
7.高階型別
1.交叉型別:多個型別合併為一個型別
function extend<T, U>(first: T, second: U): T & U { let result = <T & U>{}; for (let id in first) { (<any>result)[id] = (<any>first)[id]; } for (let id in second) { if (!result.hasOwnProperty(id)) { (<any>result)[id] = (<any>second)[id]; } } return result; } class Person1 { constructor(public name: string) { } } interface Loggable { log(): void; } class ConsoleLogger implements Loggable { log() { // ... return 11; } } var jim = extend(new Person1("Jim"), new ConsoleLogger()); var n1 = jim.name; jim.log();
2.聯合型別
聯合型別表示一個值可以是幾種型別之一。用豎線( | )分隔每個型別,所以 number | string | boolean表示一個值可以是 number, string,或 boolean。
function padLeft(value: string, padding: string | number | boolean) { // ... } let indentedString = padLeft("Hello world", true); // 如果一個值是聯合型別,我們只能訪問此聯合型別的所有型別裡共有的成員。 interface Bird { fly(); layEggs(); } interface Fish { swim(); layEggs(); } function getSmallPet(): Fish | Bird { // ... } let pet = getSmallPet(); pet.layEggs(); // okay pet.swim(); // errors
3.型別保護
let pet = getSmallPet(); if ((<Fish>pet).swim) { (<Fish>pet).swim(); } else { (<Bird>pet).fly(); }
1.自定義型別保護
function isFish(pet: Fish | Bird): pet is Fish { return (<Fish>pet).swim !== undefined; } // 'swim' 和 'fly' 呼叫都沒有問題了 if (isFish(pet)) { pet.swim(); } else { pet.fly(); }
2.typeof型別保護
function isNumber(x: any): x is number { return typeof x === "number"; } function isString(x: any): x is string { return typeof x === "string"; } function padLeft(value: string, padding: string | number) { if (isNumber(padding)) { return Array(padding + 1).join(" ") + value; } if (isString(padding)) { return padding + value; } throw new Error(`Expected string or number, got '${padding}'.`); }
3.instanceof型別保護
interface Padder { getPaddingString(): string } class SpaceRepeatingPadder implements Padder { constructor(private numSpaces: number) { } getPaddingString() { return Array(this.numSpaces + 1).join(" "); } } class StringPadder implements Padder { constructor(private value: string) { } getPaddingString() { return this.value; } } function getRandomPadder() { return Math.random() < 0.5 ? new SpaceRepeatingPadder(4) : new StringPadder(" "); } // 型別為SpaceRepeatingPadder | StringPadder let padder: Padder = getRandomPadder(); if (padder instanceof SpaceRepeatingPadder) { padder; // 型別細化為'SpaceRepeatingPadder' } if (padder instanceof StringPadder) { padder; // 型別細化為'StringPadder' }
4.可以為null的型別
let s = "foo"; s = null; // 錯誤, 'null'不能賦值給'string' let sn: string | null = "bar"; sn = null; // 可以 sn = undefined; // error, 'undefined'不能賦值給'string | null'
5.可選引數和可選屬性
使用了 --strictNullChecks,可選引數會被自動地加上 | undefined。
function f(x: number, y?: number) { return x + (y || 0); } f(1, 2); f(1); f(1, undefined); f(1, null); // error, 'null' is not assignable to 'number | undefined' // 可選屬性也會有同樣的處理: class C { a: number; b?: number; } let c = new C(); c.a = 12; c.a = undefined; // error, 'undefined' is not assignable to 'number' c.b = 13; c.b = undefined; // ok c.b = null; // error, 'null' is not assignable to 'number | undefined'
6.型別斷言
可以為null的型別是通過聯合型別實現,那麼你需要使用型別保護來去除 null。
如果編譯器不能夠去除 null或 undefined,你可以使用型別斷言手動去除。 語法是新增 !字尾: identifier!從 identifier的型別裡去除了 null和 undefined:
function broken(name: string | null): string { function postfix(epithet: string) { return name.charAt(0) + '. the ' + epithet; // error, 'name' is possibly null } name = name || "Bob"; return postfix("great"); } function fixed(name: string | null): string { function postfix(epithet: string) { return name!.charAt(0) + '. the ' + epithet; // ok } name = name || "Bob"; return postfix("great"); }
本例使用了巢狀函式,因為編譯器無法去除巢狀函式的null(除非是立即呼叫的函式表示式)。 因為它無法跟蹤所有對巢狀函式的呼叫,尤其是你將內層函式做為外層函式的返回值。 如果無法知道函式在哪裡被呼叫,就無法知道呼叫時 name的型別。
7.型別別名
型別別名會給一個型別起個新名字。 型別別名有時和介面很像,但是可以作用於原始值,聯合型別,元組以及其它任何你需要手寫的型別。
type Name = string; type NameResolver = () => string; type NameOrResolver = Name | NameResolver; function getName(n: NameOrResolver): Name { if (typeof n === 'string') { return n; } else { return n(); } } // 起別名不會新建一個型別 - 它建立了一個新名字來引用那個型別。 給原始型別起別名通常沒什麼用,儘管可以做為文件的一種形式使用。 // 同介面一樣,型別別名也可以是泛型 - 我們可以新增型別引數並且在別名宣告的右側傳入: type Container<T> = { value: T }; // 我們也可以使用型別別名來在屬性裡引用自己: type Tree<T> = { value: T; left: Tree<T>; right: Tree<T>; } // 與交叉型別一起使用,我們可以建立出一些十分稀奇古怪的型別。 type LinkedList<T> = T & { next: LinkedList<T> }; interface Person { name: string; } var people: LinkedList<Person>; var s = people.name; var s = people.next.name; var s = people.next.next.name; var s = people.next.next.next.name; // 然而,型別別名不能出現在宣告右側的任何地方。 type Yikes = Array<Yikes>; // error
8.介面 & 型別別名
介面建立了一個新的名字,可以在其它任何地方使用。型別別名並不建立新名字—比如,錯誤資訊就不會使用別名。 在下面的示例程式碼裡,在編譯器中將滑鼠懸停在 interfaced上,顯示它返回的是 Interface,但懸停在 aliased上時,顯示的卻是物件字面量型別。
type Alias = { num: number } interface Interface { num: number; } declare function aliased(arg: Alias): Alias; declare function interfaced(arg: Interface): Interface;
型別別名不能被 extends和 implements(自己也不能 extends和 implements其它型別)。 因為軟體中的物件應該對於擴充套件是開放的,但是對於修改是封閉的,你應該儘量去使用介面代替型別別名。
9.字串字面量型別
字串字面量型別允許你指定字串必須的固定值。
type Easing = "ease-in" | "ease-out" | "ease-in-out"; class UIElement { animate(dx: number, dy: number, easing: Easing) { if (easing === "ease-in") { // ... } else if (easing === "ease-out") { } else if (easing === "ease-in-out") { } else { // error! should not pass null or undefined. } } } let button = new UIElement(); button.animate(0, 0, "ease-in"); button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here // 字串字面量型別還可以用於區分函式過載: function createElement(tagName: "img"): HTMLImageElement; function createElement(tagName: "input"): HTMLInputElement; function createElement(tagName: string): Element { }
10.數字字面量型別
function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 { // ... return 1; } function foo(x: number) { if (x !== 1 || x !== 2) { // ~~~~~~~ // Operator '!==' cannot be applied to types '1' and '2'. } }
11.可辨識聯合
你可以合併單例型別、聯合型別、型別保護和型別別名來建立一個叫做【可辨識聯合的高階模式】,它也稱做【標籤聯合】或【代數資料型別】。可辨識聯合在函數語言程式設計很有用處。一些語言會自動地為你辨識聯合;而TypeScript則基於已有的JavaScript模式。它具有3個要素:
- 具有普通的單例型別屬性 — 可辨識的特徵。
- 一個型別別名包含了那些型別的聯合 — 聯合。
- 此屬性上的型別保護。
interface Square { kind: "square"; size: number; } interface Rectangle { kind: "rectangle"; width: number; height: number; } interface Circle { kind: "circle"; radius: number; }
首先我們宣告瞭將要聯合的介面。每個介面都有kind屬性但有不同的字串字面量型別。kind屬性稱做可辨識的特徵或標籤。其它的屬性則特定於各個介面。注意,目前各個介面間是沒有聯絡的。下面我們把它們聯合到一起:
type Shape = Square | Rectangle | Circle; // 現在我們使用可辨識聯合: function area(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; } }
12.完整性約束
當沒有涵蓋所有可辨識聯合的變化時,我們想讓編譯器可以通知我們。 比如,如果我們新增了 Triangle到 Shape,我們同時還需要更新 area:
type Shape = Square | Rectangle | Circle | Triangle; function area(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; } // should error here - we didn't handle case "triangle" }
有兩種方式可以實現。
1.啟用 --strictNullChecks並且指定一個返回值型別:
function area(s: Shape): number { // error: returns number | undefined switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; } }
因為 switch沒有包涵所有情況,所以TypeScript認為這個函式有時候會返回 undefined。 如果你明確地指定了返回值型別為 number,那麼你會看到一個錯誤,因為實際上返回值的型別為 number | undefined。 然而,這種方法存在些微妙之處且 --strictNullChecks對舊程式碼支援不好。
2.使用 never型別,編譯器用它來進行完整性檢查
function assertNever(x: never): never { throw new Error("Unexpected object: " + x); } function area(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; default: return assertNever(s); // error here if there are missing cases } }
這裡, assertNever檢查 s是否為 never型別—即為除去所有可能情況後剩下的型別。 如果你忘記了某個case,那麼 s將具有一個真實的型別並且你會得到一個錯誤。 這種方式需要你定義一個額外的函式,但是在你忘記某個case的時候也更加明顯。
13.多型的this型別
多型的this型別表示的是某個包含類或介面的子型別。這被稱做F-bounded多型性。它能很容易的表現連貫介面間的繼承,比如。在計算器的例子裡,在每個操作之後都返回this型別:
class BasicCalculator { public constructor(protected value: number = 0) { } public currentValue(): number { return this.value; } public add(operand: number): this { this.value += operand; return this; } public multiply(operand: number): this { this.value *= operand; return this; } // ... other operations go here ... } let v = new BasicCalculator(2) .multiply(5) .add(1) .currentValue();
由於這個類使用了 this型別,你可以繼承它,新的類可以直接使用之前的方法,不需要做任何的改變。
class ScientificCalculator extends BasicCalculator { public constructor(value = 0) { super(value); } public sin() { this.value = Math.sin(this.value); return this; } // ... other operations go here ... } let v = new ScientificCalculator(2) .multiply(5) .sin() .add(1) .currentValue();
如果沒有this型別,ScientificCalculator就不能夠在繼承BasicCalculator的同時還保持介面的連貫性。multiply將會返回BasicCalculator,它並沒有sin方法。然而,使用this型別,multiply會返回this,在這裡就是ScientificCalculator。
14.索引型別
使用索引型別,編譯器就能夠檢查使用了動態屬性名的程式碼。例如,一個常見的JavaScript模式是從物件中選取屬性的子集。
function pluck(o, names) { return names.map(n => o[n]); }
下面是如何在TypeScript裡使用此函式,通過 索引型別查詢和 索引訪問操作符:
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] { return names.map(n => o[n]); } interface Person { name: string; age: number; } let person: Person = { name: 'Jarid', age: 35 }; let strings: string[] = pluck(person, ['name']); // ok, string[]
編譯器會檢查 name是否真的是 Person的一個屬性。 本例還引入了幾個新的型別操作符。 首先是 keyof T, 索引型別查詢操作符。 對於任何型別 T, keyof T的結果為 T上已知的公共屬性名的聯合。 例如:
let personProps: keyof Person; // 'name' | 'age' // keyof Person是完全可以與 'name' | 'age'互相替換的。 不同的是如果你新增了其它的屬性到 Person,例如 address: string,那麼 keyof Person會自動變為 'name' | 'age' | 'address'。
你可以在像 pluck函式這類上下文裡使用 keyof,因為在使用之前你並不清楚可能出現的屬性名。 但編譯器會檢查你是否傳入了正確的屬性名給 pluck: pluck(person, ['age', 'unknown']); // error, 'unknown' is not in 'name' | 'age' // 第二個操作符是 T[K], 索引訪問操作符。 在這裡,型別語法反映了表示式語法。 這意味著 person['name']具有型別 Person['name'] — 在我們的例子裡則為 string型別。
然而,就像索引型別查詢一樣,你可以在普通的上下文裡使用 T[K],這正是它的強大所在。 你只要確保型別變數 K extends keyof T就可以了。 例如下面 getProperty函式的例子: function getProperty<T, K extends keyof T>(o: T, name: K): T[K] { return o[name]; // o[name] is of type T[K] } // getProperty裡的 o: T和 name: K,意味著 o[name]: T[K]。 當你返回 T[K]的結果,編譯器會例項化鍵的真實型別,因此 getProperty的返回值型別會隨著你需要的屬性改變。 let name: string = getProperty(person, 'name'); let age: number = getProperty(person, 'age'); let unknown = getProperty(person, 'unknown'); // error, 'unknown' is not in 'name' | 'age'
15.索引型別和字串索引簽名
keyof和 T[K]與字串索引簽名進行互動。 如果你有一個帶有字串索引簽名的型別,那麼 keyof T會是 string。 並且 T[string]為索引簽名的型別:
interface Map<T> { [key: string]: T; } let keys: keyof Map<number>; // string let value: Map<number>['foo']; // number
16.對映型別
對映型別指從舊型別中建立新型別
type Readonly<T> = { readonly [P in keyof T]: T[P]; } type Partial<T> = { [P in keyof T]?: T[P]; } // 像下面這樣使用: type PersonPartial = Partial<Person>; type ReadonlyPerson = Readonly<Person>;
最簡單的對映型別和它的組成部分:
type Keys = 'option1' | 'option2'; type Flags = { [K in Keys]: boolean };
它的語法與索引簽名的語法型別,內部使用了 for .. in。 具有三個部分:
- 型別變數 K,它會依次繫結到每個屬性。
- 字串字面量聯合的 Keys,它包含了要迭代的屬性名的集合。
- 屬性的結果型別。
在個簡單的例子裡, Keys是硬編碼的的屬性名列表並且屬性型別永遠是 boolean,因此這個對映型別等同於:
type Flags = { option1: boolean; option2: boolean; }
在真正的應用裡,可能不同於上面的 Readonly或 Partial。 它們會基於一些已存在的型別,且按照一定的方式轉換欄位。 這就是 keyof和索引訪問型別要做的事情:
type NullablePerson = { [P in keyof Person]: Person[P] | null } type PartialPerson = { [P in keyof Person]?: Person[P] } // 但它更有用的地方是可以有一些通用版本。 type Nullable<T> = { [P in keyof T]: T[P] | null } type Partial<T> = { [P in keyof T]?: T[P] }
在這些例子裡,屬性列表是 keyof T且結果型別是 T[P]的變體。 這是使用通用對映型別的一個好模版。 因為這類轉換是 同態的,對映只作用於 T的屬性而沒有其它的。 編譯器知道在新增任何新屬性之前可以拷貝所有存在的屬性修飾符。 例如,假設 Person.name是隻讀的,那麼 Partial<Person>.name也將是隻讀的且為可選的。
下面是另一個例子, T[P]被包裝在 Proxy<T>類裡:
type Proxy<T> = { get(): T; set(value: T): void; } type Proxify<T> = { [P in keyof T]: Proxy<T[P]>; } function proxify<T>(o: T): Proxify<T> { // ... wrap proxies ... } let proxyProps = proxify(props);
注意 Readonly<T>和 Partial<T>用處不小,因此它們與 Pick和 Record一同被包含進了TypeScript的標準庫裡:
type Pick<T, K extends keyof T> = { [P in K]: T[P]; } type Record<K extends string, T> = { [P in K]: T; } // Readonly, Partial和 Pick是同態的,但 Record不是。 因為 Record並不需要輸入型別來拷貝屬性,所以它不屬於同態: type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string> // 非同態型別本質上會建立新的屬性,因此它們不會從它處拷貝屬性修飾符。 // 由對映型別進行推斷 // 現在你瞭解瞭如何包裝一個型別的屬性,那麼接下來就是如何拆包。 其實這也非常容易: function unproxify<T>(t: Proxify<T>): T { let result = {} as T; for (const k in t) { result[k] = t[k].get(); } return result; } let originalProps = unproxify(proxyProps);
注意這個拆包推斷只適用於同態的對映型別。 如果對映型別不是同態的,那麼需要給拆包函式一個明確的型別引數。
預定義的有條件型別
- TypeScript 2.8在lib.d.ts裡增加了一些預定義的有條件型別:
- Exclude<T, U> -- 從T中剔除可以賦值給U的型別。
- Extract<T, U> -- 提取T中可以賦值給U的型別。
- NonNullable<T> -- 從T中剔除null和undefined。
- ReturnType<T> -- 獲取函式返回值型別。
- InstanceType<T> -- 獲取建構函式型別的例項型別。
例如:
type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d" type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c" type T02 = Exclude<string | number | (() => void), Function>; // string | number type T03 = Extract<string | number | (() => void), Function>; // () => void type T04 = NonNullable<string | number | undefined>; // string | number type T05 = NonNullable<(() => string) | string[] | null | undefined>; // (() => string) | string[] function f1(s: string) { return { a: 1, b: s }; } class C { x = 0; y = 0; } type T10 = ReturnType<() => string>; // string type T11 = ReturnType<(s: string) => void>; // void type T12 = ReturnType<(<T>() => T)>; // {} type T13 = ReturnType<(<T extends U, U extends number[]>() => T)>; // number[] type T14 = ReturnType<typeof f1>; // { a: number, b: string } type T15 = ReturnType<any>; // any type T16 = ReturnType<never>; // any type T17 = ReturnType<string>; // Error type T18 = ReturnType<Function>; // Error type T20 = InstanceType<typeof C>; // C type T21 = InstanceType<any>; // any type T22 = InstanceType<never>; // any type T23 = InstanceType<string>; // Error type T24 = InstanceType<Function>; // Error
注意:Exclude型別是建議的Diff型別的一種實現。我們使用Exclude這個名字是為了避免破壞已經定義了Diff的程式碼,並且我們感覺這個名字能更好地表達型別的語義。我們沒有增加Omit<T, K>型別,因為它可以很容易的用Pick<T, Exclude<keyof T, K>>來表示。