文章目錄
安裝
全局安裝TypeScript命令行工具
npm install -g typescript
安裝後就可以在全局使用tsc
命令,來編譯TypeScript文件:
tsc hello.ts
TypeScript編寫的文件後綴名是.ts
,用TypeScript編寫React應用時文件後綴名是.tsx
Hello TypeScript
function sayHello(person: string) {
return `hello, ${person}`
}
const user = 'Tom';
console.log(sayHello(user));
在TS中,使用:
指定變量的類型,編譯後的代碼:
function sayHello(person) {
return "hello, " + person;
}
var user = 'Tom';
console.log(sayHello(user));
TypeScript會對代碼進行靜態檢查,如果傳入的參數和我們指定的類型不匹配,IDE就可以給出即時的提示,並且在編譯階段會報錯(但是並不會阻止編譯的過程)
例如我們將上面的user
的值改爲數值123
,那麼在IDE中會提示:
編譯時也報錯:
src/hello.ts:5:22 - error TS2345: Argument of type '123' is not assignable to parameter of type 'string'.
5 console.log(sayHello(user));
但是仍然會生成編譯結果。如果需要在報錯時終止JS文件的生成,可以在tsconfig.json
中配置noEmitOnError
,關於tsconfig.json
後面單獨學習。
原始數據類型
字符串 + 布爾值 + 數值
JavaScript中數據分爲原始數據類型和對象類型,原始數據類型有六種(布爾值、數值、字符串、null
、undfined
和Symbol
),對於前三者類型的定義如下:
let isDone: boolean = false;
let count: number = 123;
let msg: string = 'hello'
注意,類型的定義都是針對字面量的,使用構造函數(例如new Boolean()
)創建出的變量類型時對象,而非基本類型
空值void
JavaScript中的void
JavaScript中的void
是一個運算符,用於計算它右邊的表達式,無論表達式是什麼、結果返回什麼,void
總是返回undefined
它的作用是,由於一個變量被賦值爲undefined
後,它總是可以被覆蓋,所以可以使用void
來確保可以始終返回undefined
借用這個特性,我們可以實現下面幾種效果:
(1)調用立即執行函數:
void function(){
console.log(123)
}()
(2)在函數中調用一個回調函數,但是不返回這個會帶哦函數的值:
// returning something else than undefined would crash the app
function middleware(nextCallback) {
if(conditionApplies()) {
return void nextCallback();
}
}
TypeScript中的void
JavaScript中沒有空值的概念,而在TypeScript中使用void
表示沒有任何返回值的函數:
function alertName(): void {
lert('My name is Tom');
}
如果一個變量聲明爲void
類型,那麼只能將它賦值爲undefined
或null
對於undefined
和null
,它們是所有類型的子類型,也就是說,undefined
和null
類型的變量,可以賦值其他任何類型:
let msg: string = undefined;
let count: number = null;
而void
類型的變量不是其他類型的子類型,不能賦值給其他類型的變量
let u: void;
let num: number = u;
// Type 'void' is not assignable to type 'number'.
任意值
使用any
來表示允許賦值爲任意類型,除此之外的情況,一旦定義了類型,在賦值過程中是不允許改變的:
let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
但是如果是any
類型,則可以被任意改變:
let myFavoriteNumber: any = 'seven';
myFavoriteNumber = 7;
在任意值上訪問任何屬性都是允許的,也可以調用任何方法:
let anyThing: any = 'hello';
console.log(anyThing.myName.firstName);
anyThing.myName.setFirstName('Cat');
可以認爲,聲明一個變量爲any
後,對它的任何操作,返回的內容的類型都是任意值(失去了控制)
變量在聲明時,如果沒有指定類型並且沒有賦值,那麼就會被認爲是任意類型
類型推論
如果一個變量聲明時,沒有指定類型嗎,但是進行了賦值,那麼TypeScript會依照類型推論的規則推導出一個類型
let msg = 'seven';
msg = 7;
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
它等價於
let msg: string = 'seven';
msg = 7;
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
聯合類型(Union Types)
聯合類型表示取值可以爲多種類型中的一種,使用|
來分割每個類型
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
當TypeScript不確定一個聯合類型的變量具體是哪個類型是時,我們只能訪問此聯合類型的所有類型中都共有的屬性和方法
聯合類型在賦值時也會根據類型推論被確定類型
對象的類型:接口
接口定義
在TypeScript中,我們使用接口(Interfaces)來定義對象的類型。
在面嚮對象語言中,接口是一個很重要的概念,它是對行爲的抽象,而具體如何行動需要由類(class)去實現(implement)
TypeScript中的接口既可以對類的一部分行爲進行抽象,也可以對對象的形狀(shape)進行描述,下面是一個簡單的例子:
interface Person {
name: string,
age: number,
}
const tom: Person = {
name: 'tom',
age: 100,
};
上面我們通過定義接口Person
,並且指定了tom
的類型爲Person
,這樣就約束了tom
中的形狀(也就是各個成員的類型)必須和接口Person
一致
接口名首字母大寫。
可選屬性
使用了接口的變量屬性數目必須和接口完全一致,不能多也不能少,也就是說,賦值的時候,變量的形狀必須和接口的形狀完全保持一致。
但是有些時候,我們希望不要完全匹配一個形狀,那麼就可以使用可選屬性:
interface Person {
name: string;
age?: number;
}
let tom: Person = {
name: 'Tom'
};
這個時候接口中的可選屬性是可以在變量中不存在的,但是仍然不允許添加不存在的屬性,並且可選屬性的類型(如果存在)仍需要和接口中屬性的類型一致。
任意屬性
如果希望一個接口允許有任意的屬性,可以使用[propName: string]
來定義:
interface Person {
name: string;
age?: number;
[propName: string]: any;
}
let tom: Person = {
name: 'Tom',
gender: 'male'
};
使用[propName: string]
,定義了任意屬性(屬性名類型爲string
)的類型爲any
,要注意的是,一旦定義了任意屬性,那麼確定屬性和可選屬性的類型都必須是它的類型的子集
只讀屬性
可以在接口的屬性前添加readonly
,定義此屬性是隻讀的,不能爲此屬性賦值
interface Person {
readonly name: string
}
let tom: Person = {
name: 'tom',
};
tom.name = 'jerry';
// Cannot assign to 'name' because it is a read-only property.
數組的類型
數組類型有多重定義方法:
類型 + 方括號
表示法
這是最簡的表達式方法,適用於值都是基本類型的數組:
let arr: number[] = [1, 2, 3];
arr.push('8');
// Argument of type '"8"' is not assignable to parameter of type 'number'.
使用any
表示數組中可以出現任意類型:
let list: any[] = ['xcatliu', 25, { website: 'http://xcatliu.com' }];
數組泛型
也可以使用數組泛型(Array Generic)Array<elemType>
來表示數組:
let arr: Array<number> = [1, 2, 3];
用接口表示數組
因爲數組實際上是特殊的對象,所以可以使用接口來描述數組:
interface NumberArray {
[index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];
[index: number]
限定的是索引的類型是數字的成員(實際上貌似使用propName
代替index
也可以成功)
複合數組
前面的幾種方法都定義的是數組成員是基本類型的數組,如果數組的成員是對象的話,只需要藉助接口來實現就好:
interface Item {
name: string,
age: number
}
let arr: Array<Item> = [
{ name: 'Tom', age: 100 },
{ name: 'Jerry', age: 100 },
];
類數組
類數組並不是數組,不能用普通的數組的方式來描述,而應該使用接口:
function sum() {
let args: {
[index: number]: number;
length: number;
callee: Function;
} = arguments;
}
我們通過自定義接口約束了類數組的類型,但是實際上類數組在TypeScript中都有內置的接口定義,比如arguments
對應的IArguments
,還有NodeList
、HTMLCollection
等
function sum() {
let args: IArguments = arguments;
}
IArguments
的內容實際上就是:
interface IArguments {
[index: number]: any;
length: number;
callee: Function;
}
這些內置對象在後面學習。
函數的類型
一個函數有輸入也有輸出,輸入和輸出的類型都需要進行限制
函數聲明
函數參數的個數也會被限定
function sum(x: number, y: number): number {
return x + y;
}
函數表達式
如果將上面的函數聲明改寫爲函數表達式,是這樣:
const sum = (x: number, y: number): number => {
return x + y;
};
但是實際上,這樣支隊等號右側的匿名函數進行了類型定義,等號左邊的sum
是通過賦值操作進行類型推論而推斷出來的,如果需要手動給sum
添加類型,應該是這樣:
const sum: (x: number, y: number) => number = (x: number, y: number): number => {
return x + y;
};
上面出現了兩個箭頭=>
,右側箭頭是ES6中用來定義函數的箭頭,而左側的箭頭是TypeScript中用來表示函數定義的箭頭,這個箭頭左側表示輸入類型,需要用括號括起來,右側是輸出類型。
實際上對sum
的類型的限制及限制了函數的輸入,也限制了函數的輸出。
用接口定義函數類型
也可以用接口來定義函數類型:
interface sumFn {
(x: number, y: number): number
}
const sum: sumFn = (x, y) => x + y;
接口的屬性名爲對應的函數輸入參數類型,屬性值爲函數輸出的參數類型。
可選參數
和接口的可選屬性一樣,函數的參數後面添加?
表示這個參數時可選參數,要注意的是,可選參數必須接在必須參數的後面,也就是說,可選參數後面不能出必須參數了。
function buildName(firstName: string, lastName?: string) {
if (lastName) {
return firstName + ' ' + lastName;
} else {
return firstName;
}
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
默認參數
和ES6中的函數的默認參數一樣,TypeScript中也可以給函數的參數添加默認值。
const sum = (x: number, y: number = 0): number => x + y;
sum(5, 2);
sum(5);
函數參數的默認值只能在函數中定義,不能在接口中定義:
interface sumFn {
(x: number, y: number = 0): number,
}
const sum: sumFn = (x, y) => x + y;
// Error:(9, 15) TS2371: A parameter initializer is only allowed in a function or constructor implementation.
剩餘參數
ES6中的剩餘參數需要使用數組類型來定義(因爲它就是一個數組)
function push(array: any[], ...items: any[]) {
items.forEach(function(item) {
array.push(item);
});
}
let a = [];
push(a, 1, 2, 3);
要注意,剩餘參數只能在參數的末尾出現。
函數重載
一個函數接受不同數量或者類型的參數,做出不同的處理,這種現象就是函數重載。
例如下面的函數,對輸入數字和字符串時處理的過程是不同的:
function reverse(x: number | string): number | string {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}
但是這樣並不夠精確,因爲輸入是數字的時候,也應該返回數字,輸入是字符串,也應該返回字符串,這時我們可以使用重載定義多個函數類型:
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}
前兩個都是函數定義,最後一個是函數實現,這樣做在IDE就可以看到正確的兩個提示
注意,TypeScript會從最前面的函數開始匹配,所以需要優先把精確定義寫在前面
類型斷言
類型斷言(Type Assertion)用來手動指定一個值的類型,一般用在聯合類型中,將一個不確定類型的變量指定爲聯合類型中的一種
比如,下面的函數,如果要訪問length
會報錯,因爲number
是沒有length
屬性的:
function getLength(something: string | number): number {
return something.length;
}
// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
// Property 'length' does not exist on type 'number
這個時候我們就可以使用類型斷言,將something
斷言爲string
:
function getLength(something: string | number): number {
return (<string>something).length;
}
或者:
function getLength(something: string | number): number {
return (something as string).length;
}
上面用了兩種語法類實現斷言(<類型>值)
和(值 as 類型)
,在React應用的.tsx
文件中,只能使用後一種。
要注意類型斷言不是類型轉換,不允許將類型斷言爲一個聯合類型中不存在的類型
聲明文件
聲明語句
當使用第三方庫時,我們需要引用它的聲明文件,才能獲得對應的代碼補全、接口提示等功能
比如使用jQuery時,我們獲取一個元素:
jQuery('#foo');
// ERROR: Cannot find name 'jQuery'.
但是這時編譯器並不知道$
或者jQuery
是什麼,所以我們需要使用declare
定義它的類型
declare var jQuery: (selector: string) => any;
jQuery('#foo');
上面的declare var
就是聲明語句,它並沒有定義一個變量,只是定義了jQuery
的類型,僅僅用於編譯時的檢查,編譯結果中會刪除。
聲明文件
通常情況,我們會將聲明語句放到單獨的文件中(jQuery.d.ts
),這個文件就是聲明文件:
// src/jQuery.d.ts
declare var jQuery: (selector: string) => any;
聲明文件必須以.d.ts
爲後綴,當我們將jQuery.d.ts
放入項目中,其他所有的*.ts
文件就都可以獲得jQuery
的類型定義了
如果無法解析,可以檢查一下tsconfig.json
中的files
、include
、exclude
配置,確保包含了聲明文件
第三方聲明文件
大部分熱門的第三方庫的聲明文件都不需要我們自己定義了,我們可以直接使用@types
統一管理第三方庫的聲明文件。
可以在這個頁面搜索需要的聲明文件,然後使用npm
安裝對應的聲明模塊即可:
npm install @types/jquery --save-dev
書寫聲明文件
如果第三方庫沒有提供聲明文件,我們需要自己書寫聲明瞭,在不同的場景下,聲明文件的內容和使用方式有所區別
全部變量
當通過<script>
標籤引入第三方庫的時候,會注入全局變量,就像上面的例子一樣。建議將聲明文件和源碼一起放到scr
目錄下
(1)聲明變量
可以使用declare var
和declare let
和declare const
來聲明變量,一般來說全局變量都是禁止修改的常量,所以大部分情況都應該使用const
declare const jQuery: (selector: string) => any;
要注意的是,聲明語句中只能定義類型,不要在聲明語句中定義具體的實現。
(2)聲明函數
使用declare function
來聲明全局函數的類型,jQuery
其實就是一個函數,所以也可以使用function
來定義。在函數類型的聲明語句中,也支持函數重載
declare function jQuery(cb: () => any)
declare function jQuery(selector: string): any;
(3)聲明類
使用declare class
定義一個類:
declare class Person {
name: string;
constructor(name: string) ;
sayHi(): string;
sayBye: (msg: string) => string
}
同樣的,declare class
也只能定義類型,不能定義具體實現
(4)聲明枚舉類型
JS中是沒有枚舉類型的,TypeScript中使用declare enum
聲明的枚舉類型也成爲外部枚舉,定義後的變量不能包含枚舉值之外的屬性
declare enum Direction {
up,
down,
left,
right,
}
const d1 = Direction.down;
const d2 = Direction.south;
// Error:(17, 22) TS2339: Property 'south' does not exist on type 'typeof Direction'.
(5)聲明命名空間
使用declare namespace
聲明命名空間,用來表示全局變量是一個對象,包含很多子屬性。
比如jQuery
是一個全局變量,它是一個對象,提供了一個jQuery.ajax
的方法可以調用,那麼我們就可以通過declare namespace
來聲明這個擁有很多個子屬性的全局變量:
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
}
在declare namespace
內部,直接使用function
來聲明函數,而不是使用declare function
,類似的也可以使用class
、enum
、const
等語句
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
const version: number;
class Event {
blur(eventType: EventType): void
}
enum EventType {
CustomClick
}
}
如果對象有深層的層級,則需要使用嵌套的namespace
來聲明深層的屬性的類型:
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
namespace fn {
function extend(object: any): void;
}
}
假如jQuery
下僅有fn
這一個屬性(沒有ajax
等其他屬性或方法),則可以不需要嵌套namespace
declare namespace jQuery.fn {
function extend(object: any): void;
}
(6)聲明全局的接口或類型
我們可以將接口和其他的類型放到類型聲明文件中,這樣聲明的接口或者類型就暴露成爲全局的接口或類型,可以被其他文件使用
interface AjaxSettings {
method?: 'GET' | 'POST'
data?: any;
}
declare namespace jQuery {
function ajax(url: string, settings?: AjaxSettings): void;
}
要注意,暴露在全局的interface
或者type
會作爲全局類型作用域整個項目中,存在命名衝突的可能性,所以應該儘量減少全局變量或全局類型的數量,所以應該將他們放到namespace
下:
declare namespace jQuery {
interface AjaxSettings {
method?: 'GET' | 'POST'
data?: any;
}
function ajax(url: string, settings?: AjaxSettings): void;
}
使用這個interface
的受,應該加上命名空間的前綴:
let settings: jQuery.AjaxSettings = {
method: 'POST',
data: {
name: 'foo'
}
};
(7)聲明合併
如果一個對象即是一個函數,可以直接調用jquery('#foo')
,又是一個對象,有子屬性jQuery.ajax()
,那麼可以組合多了個聲明語句,他們會不衝突的合併:
declare function jQuery(selector: string): any;
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
}
合併規則後面單獨學習。
NPM包
找到已存在的聲明文件
在給引入的NPM包創建聲明文件之前,先看它的聲明文件是否存在,一般來說可能存在於:
(1)與包綁定在一起,看package.json
的types
字段,或者看包中是否有index.d.ts
聲明文件。
推薦這種模式,因爲不需要安裝額外的其他包。我們自己創建NPM包的時候,最好也將聲明與包綁定在一起
(2)發佈到@types
裏,可以去上面提到的頁面搜索對應的聲明文件,然後安裝即可。
這種模式一般是由第三方提供的聲明文件,發佈到@types
中。
編寫聲明文件
如果沒有找到聲明文件,我們可以自己編寫。由於一般是通過import
來引入一個NPM包(假設爲foo
),所以聲明文件的存放位置有要求。
最常用的方案是,創建一個types
目錄,專門用來管理自己寫的聲明文件,將自己編寫的聲明文件放到types/foo/index.d.ts
中。
這種方式還需要配置tsconfig.json
中的paths
和baseUrl
字段。
{
"compilerOptions": {
"module": "commonjs",
"baseUrl": "./",
"paths": {
"*": ["types/*"]
}
}
}
NPM聲明文件包含下面幾種語法:
(1)export
導出變量
在NPM包的聲明文件中,如果使用declare
不會再聲明全局變量,只會在當前文件中聲明局部變量。局部變量需要使用export
導出,然後由使用方import
導入後,纔會被應用。
同樣,聲明文件中不能定義具體的實現:
// types/foo/index.d.ts
export const name: string;
export function getName(): string;
export class Animal {
constructor(name: string);
sayHi(): string;
}
export enum Directions {
Up,
Down,
Left,
Right
}
export interface Options {
data: any;
}
也可以使用declare
先聲明多個變量,然後再用export
一次性導出:
// types/foo/index.d.ts
declare const name: string;
declare function getName(): string;
declare class Animal {
constructor(name: string);
sayHi(): string;
}
declare enum Directions {
Up,
Down,
Left,
Right
}
interface Options {
data: any;
}
export {name, getName, Animal, Directions, Options};
(2)export namesapce
與declare namspace
一樣,export namespace
導出一個擁有子屬性的對象
// types/foo/index.d.ts
export namespace foo {
const name: string;
namespace bar {
function baz(): string;
}
}
可以使用export default
導出默認的function
、class
和interface
,這三者可以直接導出,其他的類型需要先定義,然後在使用export default
導出,一般會將這種導出放在整個聲明文件的最前面:
// types/foo/index.d.ts
export default Directions;
declare enum Directions {
Up,
Down,
Left,
Right
}
UMD
針對UMD格式的模塊,使用export as namespace
進行導出,一般使用時,都是先有了NPM包的聲明文件,再基於它添加export as namespace
語句,就可以將聲明好的一個變量聲明爲全局變量:
// types/foo/index.d.ts
export as namespace foo;
export = foo;
declare function foo(): string;
declare namespace foo {
const bar: number;
}
其他
可以使用declare global
來在已有的聲明文件中擴展全局變量的類型:
// types/foo/index.d.ts
declare global {
interface String {
prependHello(): string;
}
}
export {};
要注意,此聲明文件不需要導出任何東西,但是仍然導出了一個空對象,用來告訴編譯器這是一個模塊的聲明文件,而不是全局變量的聲明文件
可以使用declare module
來擴展模塊插件
自動生成聲明文件
如果庫的源碼本身就是TypeScript編寫的,那麼使用tsc
來將.ts
編譯爲JS的過程中,可以添加declaration
選項(簡寫-d
,同時生成.d.ts
聲明文件
也可以在ts.config
中添加declaration
選項來實現:
{
"compilerOptions": {
"module": "commonjs",
"outDir": "lib",
"declaration": true,
}
}
這樣就會由.ts
文件生成.d.ts
聲明文件,並且輸出到lib
目錄下:
這樣做的時候,每個.ts
文件都會對應一個.d.ts
聲明文件,這樣使用方就可以再使用import
導入時獲得類型提示
此外,tsconfig.json
中還有其他選項與自動生成聲明文件相關:
declarationDir
,設置生成.d.ts
文件的目錄declarationsMap
,對每個.d.ts
文件都生成對應的.d.ts.map
(sourcemap)文件emitDeclarationOnly
,僅僅生成.d.ts
文件,不生成.js
文件
發佈聲明文件
如果是tsc
命令自動生成的聲明文件,不需要做任何其他配置,直接發佈到NPM即可
如果是手動編寫的,需要滿足下麪條件之一,才能被正確識別:
- 給
package.json
的types
或者typings
字段指定一個類型聲明文件地址 - 在項目根目錄下,編寫
index.d.ts
文件 - 針對入口文件(
package.json
中的main
字段指定的入口文件)編寫一個同名不同後綴的.d.ts
文件
內置對象
內置對象是指根據標準在全局作用域上存在的對象,主要分爲以下幾種:
(1)ECMAScript的內置對象
例如Error
、Date
、RegExp
等,可以在TypeScript中直接將變量定義爲這些類型:
let e: Error = new Error('Error occurred');
let d: Date = new Date();
let r: RegExp = /[a-z]/;
(2)DOM和Bom的內置對象
例如Document
、HTMLElement
、Event
、NodeList
等,在TypeScript中也可以直接使用它們來定義變量類型:
let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
// Do something
});
(3)TypeScript核心庫的定義文件
TypeScript核心庫定義了所有瀏覽器環境需要用到的類型,預置在TypeScript中。當我們在使用一些常用的方法中,實際上TypeScript已經幫我們進行了類型判斷:
Math.pow(10, '2');
// index.ts(1,14): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
在TypeScript的核心庫lib.es5.d.ts
中定義了Math
的接口類型:
interface Math {
/**
* Returns the value of a base expression taken to a specified power.
* @param x The base value of the expression.
* @param y The exponent value of the expression.
*/
pow(x: number, y: number): number;
}
要注意,TypeScript核心庫中不包含Node.js部分內容。
(4)Node.js
Node.js不是內置對象,如果要使用TypeScript寫Node.js,需要引入第三方聲明文件:
npm install @types/node --save-dev