TS02 TypeScript基礎

安裝

全局安裝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中數據分爲原始數據類型和對象類型,原始數據類型有六種(布爾值、數值、字符串、nullundfinedSymbol),對於前三者類型的定義如下:

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類型,那麼只能將它賦值爲undefinednull

對於undefinednull,它們是所有類型的子類型,也就是說,undefinednull類型的變量,可以賦值其他任何類型:

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,還有NodeListHTMLCollection

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中的filesincludeexclude配置,確保包含了聲明文件

第三方聲明文件

大部分熱門的第三方庫的聲明文件都不需要我們自己定義了,我們可以直接使用@types統一管理第三方庫的聲明文件。

可以在這個頁面搜索需要的聲明文件,然後使用npm安裝對應的聲明模塊即可:

npm install @types/jquery --save-dev

書寫聲明文件

如果第三方庫沒有提供聲明文件,我們需要自己書寫聲明瞭,在不同的場景下,聲明文件的內容和使用方式有所區別

全部變量

當通過<script>標籤引入第三方庫的時候,會注入全局變量,就像上面的例子一樣。建議將聲明文件和源碼一起放到scr目錄下

(1)聲明變量

可以使用declare vardeclare letdeclare 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,類似的也可以使用classenumconst等語句

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.jsontypes字段,或者看包中是否有index.d.ts聲明文件。

推薦這種模式,因爲不需要安裝額外的其他包。我們自己創建NPM包的時候,最好也將聲明與包綁定在一起

(2)發佈到@types裏,可以去上面提到的頁面搜索對應的聲明文件,然後安裝即可。

這種模式一般是由第三方提供的聲明文件,發佈到@types中。

編寫聲明文件

如果沒有找到聲明文件,我們可以自己編寫。由於一般是通過import來引入一個NPM包(假設爲foo),所以聲明文件的存放位置有要求。

最常用的方案是,創建一個types目錄,專門用來管理自己寫的聲明文件,將自己編寫的聲明文件放到types/foo/index.d.ts中。

這種方式還需要配置tsconfig.json中的pathsbaseUrl字段。

{
  "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導出默認的functionclassinterface,這三者可以直接導出,其他的類型需要先定義,然後在使用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.jsontypes或者typings字段指定一個類型聲明文件地址
  • 在項目根目錄下,編寫index.d.ts文件
  • 針對入口文件(package.json中的main字段指定的入口文件)編寫一個同名不同後綴的.d.ts文件

內置對象

內置對象是指根據標準在全局作用域上存在的對象,主要分爲以下幾種:

(1)ECMAScript的內置對象

例如ErrorDateRegExp等,可以在TypeScript中直接將變量定義爲這些類型:

let e: Error = new Error('Error occurred');
let d: Date = new Date();
let r: RegExp = /[a-z]/;

(2)DOM和Bom的內置對象

例如DocumentHTMLElementEventNodeList等,在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

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章