走近 TypeScript 理一理那些含糊不清的概念及特性

TypeScript 是由微软开发的 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。

TypeScript 是一种给 JavaScript 添加特性的语言扩展。

下面我们会通过罗列代码的方式,来熟悉 TypeScript 的语法和一些新增的特性。

interface 接口

TypeScript 的核心原则之一是对值所具有结构进行类型检查。它有时被称做“鸭式辨型法”或“结构性子类型化”。在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约,它是 TypeScript 定义类型的一种方式,另一个是 type 之后会讲到。

interface LabelledValue {
  label: string;
}

function printLabel(labelledObj: LabelledValue) {
  console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

TypeScript 只会去关注值的外形,只要传入的对象满足接口提到的必要条件,那么它就是被允许的。其他用法:

interface Person {
  name: string,
  age: number,
  job?: string,  // 可选属性
  readonly salary: number,  // 只读属性
  [propName: string]: any    // 索引签名 可以有任意数量非定义过的属性
}
let person: Person = {
  name: ‘jack’,
  age: 28,
  job: ‘IT’,
  salary: 9999,
  id: 2345
}
function prinIt() {
  return `我是 ${person.name},我今年${person.age}, 我的工作是${person.job}, 我的薪资是${person.salary}`;
}
console.log(prinIt());
// 函数类型
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
  let result = src.search(sub);
  return result > -1;
}
// 类类型
interface ClockInterface {
    currentTime: Date;
    setTime(d: Date);
}

class Clock implements ClockInterface {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { }
}

type

TypeScript 定义类型有两种方式:接口(interface) 和类型别名(type alias),两者用法有重叠,但也有很多不同之处。

// 使用 type 定义类型
type User = {
    (name: string, age: number): void;
}

// 使用 interface 定义类型
interface Users { 
    (name: string, age: number): void;
}

Interfacetype 都可以扩展,只是语法上存在不同,两者不是相互独立的可以互相 extends。

// 1. interface extends interface
interface Name {
  name: string;
}
interface User extends Name {
  age: number;
}

// 2. type extends type
type Name = {
  name: string;
}
type User = Name & {age: number};

// 3. interface extends type
type Name = {
  name: string;
}
interface User extends Name {
  age: number;
}

// 4. type extends interface
interface Name {
  name: string;
}
type User = Name & {age: number}

type 可以声明基本类别名/联合类型/元组等类型,interface 不可以。

// 基本类型别名
type Name = string;
type Pet = Dog | Cat;

// 具体定位数组每个位置的类型
type PetList = [Dog, Pet]

// 还可以使用typeof获取实例类型赋值给 type 语句
let  div = document.createElement(‘div’);
type B = typeof div;

// 其他用法
type StringOrNumber = string | number;
type Text = string | {text: string};
type NameLookup = Dictionary<string, Person>
type Callback<T> = (data: T) => void;
type Pair<T> = [T, T];
type Coordinates = Pair<number>
type Tree<T> = T | {left: Tree<T>, right: Tree<T>}

interface 能够声明合并。

interface User {
  name: string;
  age: number;
}
interface User {
  sex: string
}
 
// User 会变成这样
interface User {
  name: string;
  age: number;
  sex: string;
}

修饰符 public private protected

默认是 public(公开),当类成员被标记为 private(私有) 时,它就不能在声明它的类的外部访问,只能在该类的内部使用。

// 使用 private 修饰
class Animal {
  private name: string;
  constructor(theName: string) {
    this.name = theName;
  }
}
let a = new Animal(‘Cat’).name; // 错误 “name” 是私有的

类成员被标记为 protected(受保护的) 时,它可以在类的内部、以及该类的子类中所访问。

// 用 protected 修饰
class Animal {
  protected name: string;
  constructor(theName: string) {
    this.name = theName;
  }
}
class Rhino extends Animal {
  constructor() {
    super('Rhino');
  }
  getName() {
    console.log(this.name); // 此处的 name 就是 Animal类中的 name
  }
}

类中的构造函数 constructor 也可以被标记 protected,这意味着这个类不能包含它的类外部被实例化,但是能被继承,也就是可以在派生类中被 super 执行。

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

// Rhino 能够继承 Animal
class Rhino extends Person {
  private food: string;
  constructor(name: string, food: string) {
    super(name);
    this.food = food;
  }
  getFood() {
    return `${this.name} love this ${this.food}`;
  }
}
let rhino = new Rhino(’zhao', ‘banana’);

基础类型 枚举 enum

TypeScript 支持与JavaScript 几乎相同的数据类型,此外还提供了枚举类型。

枚举 enum 类型是对 JavaScript 标准数据类型的一个补充,像 C# 等其他语言一样,使用枚举类型可以为一组数值赋予友好的名字。

enum Color { Red, Green, Blue }
let c: Color = Color.Green;

默认情况下,从0开始为元素编号。你也可以手动的指定成员的数值。

enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;

枚举类型提供的一个便利是你可以由枚举的值得到它的名字。例如,我们知道数值为 2,但是不确定它映射到 Color 里的哪个名字,我们可以查找相应的名字。

enum Color { Red = 1, Green, Blue }
let colorName: string = Color[2];

console.log(colorName);  // 显示'Green'因为上面代码里它的值是2

每个枚举成员可以是一个常量,也可以是一个计算变量。如果没有初始化就会被当作常数。如果项具有 TypeScript 表达式,在编译的时候就会被计算出来。

enum FileAccess {
  None,  
  Read = 1 << 1,
  Write = 1 << 2,
  ReadWrite = Read | Write
}
// 可以反向映射
enum Enum {
    A
}
let a = Enum.A;
let nameOfA = Enum[Enum.A]; // "A"
// 外部枚举使用 declare 关键字 稍后会介绍到
declare enum Enum {
  A = 1,
  B,
  C = 2
}

var re = Enum.A;
console.log(re); // 1

基础类型 元组 Tuple

元组类型 Tuple 允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。比如,你可以定义一对值分别为 string 和 number 类型的元组。

// 定义一个元组类型
let x: [string, number];
x = ['hello', 10]; // OK
x = [10, 'hello']; // Error

// 当访问一个已知索引的元素,会得到正确的类型
console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'

// 当访问一个越界的元素,会使用联合类型替代
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型
console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString
x[6] = true; // Error, 布尔不是(string | number)类型

泛型

为了考虑重用性,组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。

// 不使用泛型的函数
function identity(arg: number): number {
  return arg;
}

// 使用泛型
function identity<T>(arg: T): T {
  return arg;
}

// 泛型类
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}

// 泛型约束
interface Lengthwise {
  length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
  return arg;
}

// 在泛型中使用类型参数
function getProperty(obj: T, key: T) {
  return obj[key];
}
let x = {a: 1, b: 2, c: 3, d: 4}
getProperty(x, “a”);  // error: Argument of type ‘m’ isn’t assignable to ‘a’ | ‘b’ | ‘c’ | ‘d’
getProperty(x, “m”);

// 在泛型中使用类类型
function create<T>(c: {new(): T;}): T {
  return new c();
}

关键字 static

static 将属性声明成静态的, 这样的话就不能被继承。

class Person {
  static age: number;
  static _name: string;
  tell() {
    console.log('姓名' + Person._name);
  }
}
// 这里person是Person类的类型 这个称为“引用数据类型”
let person: Person;  
person = new Person();
Person._name = 'zhao';
person.tell();

关键字 implements

类继承接口 interface 时使用的关键字,并实现接口定义的成员。

interface Use {
  use(): void;
}
interface Person extends Use {
  eat(): void;
}
// Person 类继承接口 Person
class Person implements Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  use() { // 实现接口定义的成员
    console.log(`${this.name}, 会吃东西。`);
  }
  eat() { // 实现接口d定义的成员
    console.log(`${this.name}, 会使用g工具。`);
  }
}
const person = new Person("人");
person.use();
person.eat();

关键字 declare

定义声明文件使用 declare 进行声明,声名文件是以 .d.td 为后缀的文件。有了声明文件,编译器就可以对引入的 JavaScript 库做类型检查。

# 声明
declare getAccount(id: number): void;
declare getInfo(): any;

# 多种类型的声明
declare getData(id: number): string | number | undefined;
declare getData(id: any): any;
declare getDate(id: number): any;

# 参数可选
declare getData(id?: number): any;

# 返回的json可以单独声明成类型
declare type Result = {
  name: string,
  age: string,
  gender: boolean,
  extra: any
}
declare getAccount(id: number): Result;

# 可以声明一个 interface
declare interface Result {
  name: string,
  age: number,
  gender: boolean,
  extra: {
    loginTome: number
  }
}

关键字 abstract

使用 abstract 关键字声明的抽象类和抽象方法,子类继承抽象类后,需要实现抽象方法,抽象类不能被实例化。

abstract class Department {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  getName(): void {
    console.log('部门名称:' + this.name);
  }
  abstract meeting(): void; // 抽象方法必须在派生类中实现
}

class AccountingDepartment extends Department {
  constructor() {
    super('会计和审查'); // 在派生类中必须调用super();
  }
  meeting(): void {
    console.log('会计每个星期一上午开会');
  }
  generateReports(): void {
    console.log('生成会议报告');
  }
}
let department: Department = new AccountingDepartment();

//抽象类不能实例化
// let department: Department = new Department();

department.getName();
department.meeting();

// 此方法不能调用,因为在声明的抽象类中不存在
// department.generateReports(); 

namespace 命名空间

关于术语的一点说明: 请务必注意一点,TypeScript 1.5 里术语名已经发生了变化。 “内部模块”现在称做“命名空间”。 “外部模块”现在则简称为“模块”,这是为了与 ECMAScript 2015 里的术语保持一致,(也就是说 module X { } 相当于现在推荐的写法 namespace X { } )。

namepace 一种来组织代码,以便于在记录它们类型的同时还不用担心与其它对象产生命名冲突的一种手段。

// 不使用命名空间的代码
interface Animal {
  name: string;
  eat(): void;
}
class Dog implements Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  eat() {
    console.log(`${this.name} 吃骨头`);
  }
}

class Cat implements Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  eat() {
    console.log(`${this.name} 吃小鱼`);
  }
} 


// 使用 namespace 修改上边的例子
namespace Zoom {
  export interface Animal {
    name: string;
    eat(): void;
  }
  export class Dog implements Animal {
    name: string;
    constructor(name: string) {
      this.name = name;
    }
    eat() {
      console.log(`${this.name} 吃骨头`);
    }
  }
  // 类继承接口
  export class Cat implements Animal {
    name: string;
    constructor(name: string) {
      this.name = name;
    }
    eat() {
      console.log(`${this.name} 吃小鱼`);
    }
  }
}
let dog: Zoom.Animal;
dog = new Zoom.Dog("小狗");
dog.eat();

通常情况下,声明的命名空间代码和调用的代码不在同一个文件里。通过三斜线指令 /// 引用命名空间,即可通过"完全限定名"进行访问。

// 三斜线指令 
/// <reference path=“zoom.ts”>

let dog: Zoom.Animal;
dog = new Zoom.Dog("小狗");
dog.eat();

在引用命名空间别名时,可以通过 import 关键字起一个别名。

// 命名空间别名
/// <reference path=“zoom.ts”/>
/// <reference path=“cat.ts”/>
/// <reference path=“dog.ts”/>

import bio_other = zoom; // 给zoom起一个别名
let dog = bio_other.Animal;
dog = new bio_other.Dog("小狗");
dog.eat();

三斜线指令

三斜线指令 /// 是包含单个 XML 标签的单行注释, 注释的内容会做为编译器指令使用。

三斜线指令 /// 仅可放在包含它的文件的最顶端。一个三斜线指令的前面只能出现单行或多行注释,这包括其它的三斜线指令。如果它们出现在一个语句或声明之后,那么它们会被当做普通的单行注释,并且不具有特殊的涵义。

/// <reference path="..." />
// 上边指令是三斜线指令中最常见的一种,用于声明文件间的依赖。
// 三斜线引用告诉编译器在编译过程中要引入的额外的文件。

编译器会对输入文件进行预处理来解析所有三斜线 /// 引用指令。在这个过程中,额外的文件会加到编译过程中。

引用不存在的文件会报错, 一个文件用三斜线指令 /// 引用自己会报错。

模块 module

TypeScript 沿用 ECMAScrept 2015 的模块概念,任何包含顶级 import 或 export 的文件都被当成一个模块。模块在自身作用域里执行,而不是全局作用域里。

导出

可以使用 export 向外界导出模块。

// # 任何声明(比如变量,函数,类,类型别名或接口)都能够通过添加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 };


// # 导出的内容联合在一起通过语法:export * from "module"。
export * from "./StringValidator"; // exports interface StringValidator
export * from "./LettersOnlyValidator"; // exports class LettersOnlyValidator
export * from "./ZipCodeValidator";  // exports class ZipCodeValidator

// # 默认导出
// # 每个模块都可以有一个default导出。 默认导出使用 default 关键字标记,并且一个模块只能够有一个default导出。
// JQuery.d.ts 
declare let $: JQuery;
export default $; // 从声明文件中默认导出

导入

可以使用 import 来导入其它模块中的导出内容。

// # 导入一个模块中某个导出内容
import { ZipCodeValidator } from "./ZipCodeValidator";
let myValidator = new ZipCodeValidator();

// # 使用 as 对导入的模块重命名
import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
let myValidator = new ZCV();

// # 将整个模块导入到一个变量,并通过它来访问模块的导出部分
import * as validator from "./ZipCodeValidator";
let myValidator = new validator.ZipCodeValidator();

// # 导入默认导出的模块 不加 {}

TypeScript 新增 export = 和 import = require()

CommonJS 和 AMD 的环境里都有一个 exports 变量,这个变量包含了一个模块的所有导出内容。

exports 类似于 es6 语法里的默认导出 export default。虽然作用相似,但是 export default 语法并不能兼容 CommonJS 和 AMD 的 exports

为了支持 CommonJS 和 AMD 的 exports,TypeScript 提供了 export = 语法。

export = 语法定义一个模块的导出对象(类,接口,命名空间,函数或枚举)。若使用 export = 导出一个模块,则必须使用 TypeScript 的特定语法 import module = require("module") 来导入此模块。

// # 使用 export = 导出模块
let numberRegexp = /^[0-9]+$/;
class ZipCodeValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
export = ZipCodeValidator;


// # 使用 import module = require('module') 导入模块
import zip = require("./ZipCodeValidator");
let strings = ["Hello", "98052", "101"];
let validator = new zip();
strings.forEach(s => {
  console.log(`"${ s }" - ${ validator.isAcceptable(s) ? "matches" : "does not match" }`);
});

命名空间 namespace 于模块 module 的区别:

  • 命名空间 namespace:代码层面的归类和管理。防止命名重复,将有相似功能的代码都归一到同一个空间下进行管理,方便其他代码引用。更多的是侧重代码的复用。
  • 模块 module:一个完成功能的封装,对外提供的是一个具有完整功能的功能包。需要显示引用,一个文件一个模块,一个模块可能会有多个命名空间。

最后,希望通过本文可以帮助 TypeScript 初学者包括我对了解和熟悉 TypeScript 的语法和新增特性能有一个直观和感性的认识。我相信很多人,在涉足未知领域或者在学习一门新的技术时,开始都会不知道从何下手,内心可能还会有一种畏惧感,遇到这种情况,我们需要迈出第一步,先从熟悉语法开始,可以多看别人写的代码,不懂的地方通过查阅文档和相关资料,接触多了耳濡目染,最后做到知其然而知其所以然,我相信大家都是最棒的。

原文发布于微信公众号 - 前端infoQ(webinfoq)

原文发表时间:2019-06-27

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

编辑于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券