TypeScript 入門系列 | TypeScript 基礎(一)

背景介紹

摘自維基百科 TypeScript前端

TypeScript是一種由微軟開發的自由和開源的編程語言。它是JavaScript的一個嚴格超集,並添加了可選的靜態類型和使用看起來像基於類的面向對象編程語法操做 Prototype。C#的首席架構師以及Delphi和Turbo Pascal的創始人安德斯·海爾斯伯格參與了TypeScript的開發。git

  • TypeScript 設計目標是開發大型應用,而後轉譯成 JavaScript。因爲 TypeScript 是 JavaScript 的嚴格超集,任何現有的 JavaScript 程序都是合法的 TypeScript 程序。
  • TypeScript 支持爲現存 JavaScript 庫添加類型信息的定義文件,方便其餘程序像使用靜態類型的值同樣使用現有庫中的值。當前有第三方提供經常使用庫如 jQuery、MongoDB、Node.js 和 D3.js 的定義文件。
  • TypeScript 編譯器自己也是用 TypeScript 編寫,並被轉譯爲 JavaScript,以 Apache License 2 發佈。

GitHub 地址 github.com/microsoft/T…程序員

TypeScript 官網 www.typescriptlang.orggithub

如今有不少大型開源項目都開始使用 TypeScripttypescript

TypeScript 或許不久會成爲前端的一個強制要求技能。在維護大型項目方面它很是有效。npm

下面開始行動起來。。。編程

安裝

npm install -g typescript
複製代碼

編寫基本代碼json

// greeter.ts
function greeter(person: string) {
   return "Hello, " + person;
}

let user = "Jane User";

document.body.innerHTML = greeter(user);
複製代碼

編譯代碼數組

tsc greeter.ts
複製代碼

基礎類型

布爾值

let isDone: boolean = false;
複製代碼

數字

TypeScript 裏的全部數字都是浮點數。 這些浮點數的類型是 number。 除了支持十進制和十六進制字面量,TypeScript 還支持 ECMAScript 2015 中引入的二進制和八進制字面量。bash

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;
複製代碼

字符串

let name: string = "bob";
複製代碼

數組

// 方式1: 能夠在元素類型後面接上 [],表示由此類型元素組成的一個數組
let list: number[] = [1, 2, 3];
// 方式2: 使用數組泛型,Array<元素類型>:
let list: Array<number> = [1, 2, 3];
複製代碼

元組 Tuple

元組類型容許表示一個已知元素數量和類型的數組,各元素的類型沒必要相同。 好比,你能夠定義一對值分別爲 stringnumber 類型的元組。

// Declare a tuple type
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
// Initialize it incorrectly
x = [10, 'hello']; // Error
複製代碼

當訪問一個越界的元素,會使用聯合類型替代

x[3] = 'world';
x[6] = true; // Error, 布爾不是(string | number)類型
複製代碼

枚舉

enum 類型是對 JavaScript 標準數據類型的一個補充。

enum Color {Red, Green, Blue}
let c: Color = Color.Green;
// 默認狀況下,從0開始爲元素編號, 也能夠手動的指定成員的數值
enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;
複製代碼

測試枚舉類型

enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;
console.log(Color);
複製代碼

編譯以後

var Color;
(function (Color) {
   Color[Color["Red"] = 1] = "Red";
   Color[Color["Green"] = 2] = "Green";
   Color[Color["Blue"] = 3] = "Blue";
})(Color || (Color = {}));
var c = Color.Green;
console.log(Color);
複製代碼

執行的結果

{ '1': 'Red', '2': 'Green', '3': 'Blue', Red: 1, Green: 2, Blue: 3 }
複製代碼

Any

有時候,咱們會想要爲那些在編程階段還不清楚類型的變量指定一個類型。 這些值可能來自於動態的內容,好比來自用戶輸入或第三方代碼庫。 這種狀況下,咱們不但願類型檢查器對這些值進行檢查而是直接讓它們經過編譯階段的檢查。

let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)
複製代碼

Void

某種程度上來講,void 類型像是與 any 類型相反,它表示沒有任何類型。 當一個函數沒有返回值時,你一般會見到其返回值類型是 void

聲明一個 void 類型的變量沒有什麼大用,由於你只能爲它賦予 undefinednull

let unusable: void = undefined;
複製代碼

Null 和 Undefined

undefinednull 二者各自有本身的類型分別叫作 undefinednull。 和 void類似,它們的自己的類型用處不是很大

let u: undefined = undefined;
let n: null = null;
複製代碼

默認狀況下 nullundefined 是全部類型的子類型。 就是說你能夠把 nullundefined 賦值給 number 類型的變量。

當你指定了 --strictNullChecks 標記,nullundefined 只能賦值給 void 和它們各自。

Never

never 類型表示的是那些永不存在的值的類型。 例如,never 類型是那些老是會拋出異常或根本就不會有返回值的函數表達式或箭頭函數表達式的返回值類型; 變量也多是 never 類型,當它們被永不爲真的類型保護所約束時。

never 類型是任何類型的子類型,也能夠賦值給任何類型;然而,沒有類型是 never 的子類型或能夠賦值給 never 類型(除了 never 自己以外)。 即便 any 也不能夠賦值給 never

// 返回never的函數必須存在沒法達到的終點
function error(message: string): never {
    throw new Error(message);
}
複製代碼
// 返回never的函數必須存在沒法達到的終點
function infiniteLoop(): never {
    while (true) {
    }
}
複製代碼

Object

object 表示非原始類型,也就是除 numberstringbooleansymbolnullundefined 以外的類型。

使用 object 類型,就能夠更好的表示像 Object.create 這樣的API

declare function create(o: object | null): void;

create({ prop: 0 }); // OK
create(null); // OK
create(42); // Error
複製代碼

類型斷言

經過類型斷言這種方式能夠告訴編譯器,「相信我,我知道本身在幹什麼」。 類型斷言比如其它語言裏的類型轉換,可是不進行特殊的數據檢查和解構。 它沒有運行時的影響,只是在編譯階段起做用。 TypeScript 會假設你,程序員,已經進行了必須的檢查。

類型斷言有兩種形式。 其一是「尖括號」語法

let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;
複製代碼

另外一個爲 as 語法

let someValue: any = "this is a string";

let strLength: number = (someValue as string).length;
複製代碼

兩種形式是等價的。 至於使用哪一個大多數狀況下是憑我的喜愛;然而,當你在 TypeScript 裏使用 JSX 時,只有 as 語法斷言是被容許的。

接口

可選屬性

可選屬性在應用 「option bags」 模式時很經常使用,即給函數傳入的參數對象中只有部分屬性賦值了。

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"});
複製代碼

帶有可選屬性的接口與普通的接口定義差很少,只是在可選屬性名字定義的後面加一個 ? 符號。

只讀屬性

一些對象屬性只能在對象剛剛建立的時候修改其值。 你能夠在屬性名前用 readonly 來指定只讀屬性

interface Point {
    readonly x: number;
    readonly y: number;
}
複製代碼

能夠經過賦值一個對象字面量來構造一個 Point 。 賦值後, xy 不再能被改變了。

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
複製代碼

TypeScript 具備 ReadonlyArray<T> 類型,它與 Array<T> 類似,只是把全部可變方法去掉了,所以能夠確保數組建立後不再能被修改

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!
複製代碼

上面代碼的最後一行,能夠看到就算把整個 ReadonlyArray 賦值到一個普通數組也是不能夠的。 可是你能夠用類型斷言重寫

a = ro as number[];
複製代碼

readonly vs const

最簡單判斷該用 readonly 仍是 const 的方法是看要把它作爲變量使用仍是作爲一個屬性。 作爲變量使用的話用 const,若作爲屬性則使用 readonly

函數類型

爲了使用接口表示函數類型,咱們須要給接口定義一個調用簽名。 它就像是一個只有參數列表和返回值類型的函數定義。參數列表裏的每一個參數都須要名字和類型。

interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
  let result = source.search(subString);
  return result > -1;
}
複製代碼

可索引的類型

可索引類型具備一個 索引簽名,它描述了對象索引的類型,還有相應的索引返回值類型。

interface StringArray {
  [index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
複製代碼

類類型

interface ClockInterface {
    currentTime: Date;
}
class Clock implements ClockInterface {
    currentTime: Date;
    constructor(h: number, m: number) { }
}
複製代碼

也能夠在接口中描述一個方法,在類裏實現它

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 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;
複製代碼

混合類型

interface Counter {
    (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;
複製代碼

接口繼承類

接口繼承了一個類類型時,它會繼承類的成員但不包括其實現。 就好像接口聲明瞭全部類中存在的成員,但並無提供具體實現同樣。 接口一樣會繼承到類的 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,這對私有成員的兼容性是必需的。

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");
複製代碼

繼承

class Animal {
    move(distanceInMeters: number = 0) {
        console.log(`Animal moved ${distanceInMeters}m.`);
    }
}

class Dog extends Animal {
    bark() {
        console.log('Woof! Woof!');
    }
}

const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();
複製代碼

公共,私有與受保護的修飾符

默認爲 public

class Animal {
    public name: string;
    public constructor(theName: string) { this.name = theName; }
    public move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}
複製代碼

理解 private

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

new Animal("Cat").name; // 錯誤: 'name' 是私有的.
複製代碼

TypeScript使用的是結構性類型系統。 當咱們比較兩種不一樣的類型時,並不在意它們從何處而來,若是全部成員的類型都是兼容的,咱們就認爲它們的類型是兼容的。

當咱們比較帶有 privateprotected 成員的類型的時候,狀況就不一樣了。 若是其中一個類型裏包含一個 private成員,那麼只有當另一個類型中也存在這樣一個 private成員, 而且它們都是來自同一處聲明時,咱們才認爲這兩個類型是兼容的。 對於 protected成員也使用這個規則。

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

class Rhino extends Animal {
    constructor() { super("Rhino"); }
}

class Employee {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");

animal = rhino;
animal = employee; // 錯誤: Animal 與 Employee 不兼容,若是 private 都變成 public 則不會報錯
複製代碼

理解 protected

protected 修飾符與 private 修飾符的行爲很類似,但有一點不一樣, protected 成員在派生類中仍然能夠訪問。

class Person {
 // 這裏若是變成 private 子類用 `this.name` 會報錯
  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 Person {
    protected name: string;
    protected constructor(theName: string) { this.name = theName; }
}

// Employee 可以繼承 Person
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");
let john = new Person("John"); // 錯誤: 'Person' 的構造函數是被保護的.
複製代碼

readonly修飾符

class Octopus {
    readonly name: string;
    readonly numberOfLegs: number = 8;
    constructor (theName: string) {
        this.name = theName;
    }
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // 錯誤! name 是隻讀的.
複製代碼

靜態屬性

咱們也能夠建立類的靜態成員,這些屬性存在於類自己上面而不是類的實例上。

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}));
複製代碼

抽象類

抽象類作爲其它派生類的基類使用。 它們通常不會直接被實例化。 不一樣於接口,抽象類能夠包含成員的實現細節。

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() // 錯誤: 方法在聲明的抽象類中不存在

複製代碼

函數

爲函數定義類型

function add(x: number, y: number): number {
    return x + y;
}
let myAdd = function(x: number, y: number): number { return x + y; };
複製代碼

推斷類型

// myAdd has the full function type
let myAdd = function(x: number, y: number): number { return x + y; };
// The parameters `x` and `y` have the type number
let myAdd: (baseValue: number, increment: number) => number = function(x, y) { return x + y; };
複製代碼

可選參數和默認參數

function buildName(firstName: string, lastName: string) {
    return firstName + " " + lastName;
}
let result1 = buildName("Bob");                  // error, too few parameters
let result2 = buildName("Bob", "Adams", "Sr.");  // error, too many parameters
let result3 = buildName("Bob", "Adams");         // ah, just right
複製代碼

JavaScript 裏,每一個參數都是可選的,可傳可不傳。 沒傳參的時候,它的值就是 undefined。 在 TypeScript 裏咱們能夠在參數名旁使用 ? 實現可選參數的功能

function buildName(firstName: string, lastName?: string) {
    // ...
}
複製代碼

剩餘參數

在 TypeScript 裏,你能夠把全部參數收集到一個變量裏

function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfName.join(" ");
}
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
複製代碼

泛型

咱們須要一種方法使返回值的類型與傳入參數的類型是相同的。這裏,咱們使用了類型變量,它是一種特殊的變量,只用於表示類型而不是值。

function identity<T>(arg: T): T {
    return arg;
}
複製代碼

咱們給 identity 添加了類型變量 TT 幫助咱們捕獲用戶傳入的類型(好比:number),以後咱們就可使用這個類型。 以後咱們再次使用了 T 當作返回值類型。如今咱們能夠知道參數類型與返回值類型是相同的了。 這容許咱們跟蹤函數裏使用的類型的信息。

使用泛型變量

使用泛型建立像 identity 這樣的泛型函數時,編譯器要求你在函數體必須正確的使用這個通用的類型。 換句話說,你必須把這些參數當作是任意或全部類型。

若是咱們想同時打印出arg的長度。 咱們極可能會這樣作

function loggingIdentity<T>(arg: T[]): T[] {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}
複製代碼

如今假設咱們想操做 T 類型的數組而不直接是 T。因爲咱們操做的是數組,因此 .length 屬性是應該存在的。 咱們能夠像建立其它數組同樣建立這個數組:

function loggingIdentity<T>(arg: T[]): T[] {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}
複製代碼

泛型類型

泛型函數的類型與非泛型函數的類型沒什麼不一樣,只是有一個類型參數在最前面,像函數聲明同樣

function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: <T>(arg: T) => T = identity;
複製代碼

咱們也可使用不一樣的泛型參數名,只要在數量上和使用方式上能對應上就能夠。

function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: <U>(arg: U) => U = identity;
複製代碼

還可使用帶有調用簽名的對象字面量來定義泛型函數(這塊沒看懂)

function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: {<T>(arg: T): T} = identity;
複製代碼

泛型類

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; };
複製代碼

泛型約束

相比於操做 any 全部類型,咱們想要限制函數去處理任意帶有 .length 屬性的全部類型。 只要傳入的類型有這個屬性,咱們就容許,就是說至少包含這一屬性。 爲此,咱們須要列出對於 T 的約束要求。

interface Lengthwise {
    length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}
複製代碼

枚舉

數字枚舉

enum Direction {
    Up = 1,
    Down,
    Left,
    Right
}
複製代碼

使用枚舉

enum Response {
    No = 0,
    Yes = 1,
}
function respond(recipient: string, message: Response): void {
    // ...
}
respond("Princess Caroline", Response.Yes)
複製代碼

字符串枚舉

enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}
複製代碼

運行時的枚舉

枚舉是在運行時真正存在的對象

enum E {
    X, Y, Z
}
function f(obj: { X: number }) {
    return obj.X;
}
// Works, since 'E' has a property named 'X' which is a number.
f(E);
複製代碼

反向映射

除了建立一個以屬性名作爲對象成員的對象以外,數字枚舉成員還具備了 反向映射,從枚舉值到枚舉名字

enum Enum {
    A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
複製代碼
// 編譯後
var Enum;
(function (Enum) {
    Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
var a = Enum.A;
var nameOfA = Enum[a]; // "A"
複製代碼

生成的代碼中,枚舉類型被編譯成一個對象,它包含了正向映射( name -> value)和反向映射( value -> name)。 引用枚舉成員總會生成爲對屬性訪問而且永遠也不會內聯代碼。

const 枚舉

爲了不在額外生成的代碼上的開銷和額外的非直接的對枚舉成員的訪問,咱們可使用 const 枚舉。 常量枚舉經過在枚舉上使用 const 修飾符來定義。

const enum Enum {
    A = 1,
    B = A * 2
}
複製代碼

常量枚舉只能使用常量枚舉表達式,而且不一樣於常規的枚舉,它們在編譯階段會被刪除。常量枚舉成員在使用的地方會被內聯進來。 之因此能夠這麼作是由於,常量枚舉不容許包含計算成員。

const enum Directions {
    Up,
    Down,
    Left,
    Right
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]
複製代碼
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
複製代碼

外部枚舉

declare

declare enum Enum {
    A = 1,
    B,
    C = 2
}
複製代碼

外部枚舉和非外部枚舉之間有一個重要的區別,在正常的枚舉裏,沒有初始化方法的成員被當成常數成員。對於很是數的外部枚舉而言,沒有初始化方法時被當作須要通過計算的。


公衆號,歡迎前往關注

相關文章
相關標籤/搜索