专栏名称: 云影sky
前端
目录
相关文章推荐
今天看啥  ›  专栏  ›  云影sky

TypeScript 入门系列 | TypeScript 基础(一)

云影sky  · 掘金  ·  · 2019-08-31 17:48
阅读 15

TypeScript 入门系列 | TypeScript 基础(一)

背景介绍

摘自维基百科 TypeScript

TypeScript是一种由微软开发的自由和开源的编程语言。它是JavaScript的一个严格超集,并添加了可选的静态类型和使用看起来像基于类的面向对象编程语法操作 Prototype。C#的首席架构师以及Delphi和Turbo Pascal的创始人安德斯·海尔斯伯格参与了TypeScript的开发。

  • 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.org

现在有很多大型开源项目都开始使用 TypeScript

TypeScript 或许不久会成为前端的一个强制要求技能。在维护大型项目方面它非常有效。

下面开始行动起来。。。

安装

npm install -g typescript
复制代码

编写基本代码

// 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 中引入的二进制和八进制字面量。

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
}
复制代码

外部枚举和非外部枚举之间有一个重要的区别,在正常的枚举里,没有初始化方法的成员被当成常数成员。对于非常数的外部枚举而言,没有初始化方法时被当做需要经过计算的。


公众号,欢迎前往关注




原文地址:访问原文地址
快照地址: 访问文章快照