TypeScript基础知识总结

一、什么是TypeScript?

TypeScript是由微软开发的一门脚本语言,它是JavaScript的一个超集,完全兼容JavaScript。也就是说,任何合法的JavaScript代码,也都是合法的TypeScript代码。

TypeScript以.ts结尾,经过编译器编译,可生成常规的.js代码文件。如:
hello.ts

let msg: string = 'hello world';
function outputMsg (msg: string):void {
  console.log(msg);
}

outputMsg(msg);

这里的msg: string是将变量msg声明为字符串类型,之后只要是给该变量赋值为其他任何类型,TypeScript编译器都将报错。:void则是声明该函数没有返回值,如果该函数带有返回值,编译器也将报错。

假设我们已经通过npm install -g typescript安装了TypeScript,我们就可以在控制台运行如下命令来生成对应的js代码:

tsc hello.ts

现在hello.ts的相同目录下会生成一个hello.js文件:

let msg = 'hello world';
function outputMsg (msg) {
  console.log(msg);
}

outputMsg(msg);

我们看到,TypeScript编译器去掉了与变量或函数返回类型相关的类型声明,生成了原生的js代码,这个js代码即可作为静态资源使用。

TypeScript出现的目的是解决JavaScript缺少静态类型检查,因此不适合开发大型项目的问题。它在JavaScript的基础上新增了类型检查、类型推断、接口、泛型等一些属于静态语言的特性,然后通过TypeScript编译器编译为纯JavaScript代码,以运行在各大平台之上。

TypeScript被设计为JavaScript的超集,这使得从JavaScript过渡到TypeScript的代价非常低。甚至即使你直接把js文件改名为.ts后缀,也可以正常被TypeScript的编译器所编译。使用TypeScript几乎已经是前端开发的必然趋势,下面我们就来学习一下TypeScript的入门知识吧。

:本文只探讨TypeScript的新增语法,JavaScript的基础语法不在本文列举。

二、TypeScript基础

1. TypeScript的安装

安装TypeScript非常简单,只需要运行如下命令:

npm install -g typescript

现在我们的全局npm目录下就新增了TypeScript的编译器包,它提供的tsc命令可以将一个.ts后缀的文件编译为js文件。如下命令可以编译一个ts文件:

tsc hello.ts
// 生成hello.js

如果要在开发工具中使用TypeScript,只需要安装对应的插件即可。

新版本的VS Code已经默认启用了对TypeScript的支持,因此无需额外安装插件。如果需要使用webpack对包含ts代码的项目进行打包,可以配置对应的loader:ts-loader,它会在打包时自动将ts代码转换为js代码生成出来:
webpack.config.js

{
  test: /\.tsx?$/,
  loader: 'ts-loader',
  exclude: /node_modules/,
  options: {
    appendTsSuffixTo: [/\.vue$/],
  }
}

项目的根目录下需要新增tsconfig.json,以配置TypeScript的行为:

{
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules"
  ],
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "allowJs": true,
    "module": "es2015",
    "target": "es5",
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "isolatedModules": true,
    "lib": [
      "dom",
      "es5",
      "es2015.promise"
     ],
    "sourceMap": true,
    "pretty": true
  }
}

以上配置的含义在这里不再详述,可参考官网手册tsconfig.json

2. 数据类型

TypeScript是一种强类型脚本语言,它要求程序应该明确指定变量或函数返回值的类型(即使这个类型是any)。TypeScript约定的类型如下:

数据类型 关键字 说明
任意类型 any 不限制变量类型,该关键字常用于兼容JavaScript代码
数字类型 number 对应JavaScript的number
字符串类型 string 对应JavaScript的string
布尔类型 boolean 对应JavaScript的boolean
数组类型 - TypeScript的数组没有特定关键字,它的声明方式由每一项的类型决定。如数字类型的数组,类型为number[];或以泛型表示为:Array<number> 。TypeScript的数组元素只能是同一种数据类型。
元组 - 带有不同数据类型的特定数组在TypeScript中称为元组,如let x: [string, number]
枚举 enum 与静态语言的枚举类型一致
void void 空,表示函数没有返回值
null null 空对象
undefined undefined 变量未初始化
never never 代指永远不可能出现的值,常用于无法执行完毕或结束语句不可达的函数的返回类型

这里我们只介绍几个TypeScript中较为特殊的几个数据类型。

  1. any:表示该变量允许为任意类型。如:
let a: any = 123;
a = '123';
a = {};  

为变量赋值为任意类型均不会报错。在改造现有的JavaScript项目时,这种数据类型非常有用,因为既有项目的某些变量可能无法保证只保存一种类型的数据。

  1. 数组:对应JavaScript的数组,但是要求每一项的数据类型必须一致。声明数组主要有以下两种方式:
// 一般语法
let arr: number[] = [1, 2, 3];

// 泛型语法
let array: Array<number> = [4, 5, 6];
  1. 元组:对上述类型的补充,它允许数组的数据类型不一致,但是必须与定义时指定的数据类型一致。如:
let author: [string, number] = ['carter', 25];

author = [25, 'carter'];  // 编译报错,与元组类型不一致
  1. 枚举:定义一组常量,常用于消除魔术字符串。如:
enum Color {Red, Green};
let c: Color = Color.Green;
console.log(c);    // 输出 1

enum Color {Red, Green, Blue}被编译后会生成如下代码:

var Color;
(function (Color) {
    Color[Color["Red"] = 0] = "Red";
    Color[Color["Green"] = 1] = "Green";
})(Color || (Color = {}));
;

它大致是这样一个对象:

var Color = {
  "0": "Red",
  "Red": "Red",
  "1": "Green",
  "Green": "Green",
}
  1. void:表示函数无返回值。熟悉java或者c等静态语言的应该经常见到了:
function output():void {
  console.log('123');
}
  1. never:表示函数不会结束或函数的结束语句不可达。如:
function error():never {
  throw new Error('error msg');
}

function doWhile():never {
  while(1){ ... }
}

上述两个函数都无法正常结束,因此它们的返回值是never。

如果一个变量可能存在多种变量类型,并且这些类型是已知的,那么可以通过联合类型罗列出所有可能使用的类型,如:

function f(arg: number | string):void {
  ...
}

f(123);
f('123');

3. 类型断言(Type Assertion)

类型断言的目的是手动将一个变量指定为某种数据类型。为什么需要手动指定呢?

这是因为TypeScript需要对代码进行静态分析,当变量可能拥有多种数据类型时,对TypeScript来说,只有这些数据类型都支持的操作才是安全的操作,如:

function f(v: string | number):number {
  if (v.length) {
    ...
  } else {
    ...
  }
}

该函数接收一个参数,类型可为字符串或数字。假设传入的参数是数字,那么length将是不存在的,函数执行就会报错。为了防止这种情况发生,TypeScript只能抛出错误,告知string | number这种联合类型的变量不能访问length属性。

然而如果在某些情况下你明确知道变量的类型,则可以通过类型断言的方式告知TypeScript编译器,如:

function f(v: string | number):number {
  if ((<string>v).length) {
  // 或if ((v as string).length) {
    ...
  } else {
    ...
  }
}

<string>vv as string这两种语法都可以将变量v的数据类型指定为字符串,这样在访问length属性时,编译器就不会报错了。

这种语法看上去很像java中的强制类型转换,但其实它们不是一回事。java中的强制类型转换存在一个转换过程,涉及到某些编译操作;而TypeScript的类型断言并未进行任何类型转换,它只是根据断言结果启用了更加明确的编译规则。

类型断言使得变量也可以跨类型赋值,如:

var str = '1' 
var str2: number = <number> <any> str;
var str3: number = str2;

我们看到,str是字符串,而str2是数值类型,按理说无法直接赋值。但是我们通过<number> <any> str的语法,先是将str断言成any类型,相当于暂时消除了str原本的数据类型,然后又从any断言为number,这样它就可以被赋值给str了。赋值后的变量str2仍然是数值 类型,因此它可以被赋值给数值类型的str3。

这里我们必须先把字符串断言为any,才能再断言为number,因为number和string之间不允许直接进行类型转换断言。

通常来说,跨类型的变量赋值在TypeScript中是不允许的,类型断言给了我们权限来跳过这项规则。

如果在编写TypeScript代码时没有明确指定变量类型,那么TypeScript会根据初始化的值来推测数据类型,并以此作为依据进行后续的类型推断:
demo.ts

let a = 123;
a = '123';  // 这在TypeScript编译时会报错

由于a被赋值为123,因此它的数据类型被推断为数值,后续为其赋值字符串时就会看到一条类型推断的报错。不过即使有报错,对应的js代码依然可以生成(可以通过命令参数阻止TypeScript在有编译错误时生成js代码)。

4. 函数

TypeScript中函数的主要变化体现在参数和返回值上。

关于返回值,上文已经提及,可以用以下语法定义返回值类型:

function f(): number {
  return 123;
}

关于参数,有几点不同。

首先,TypeScript指定的参数是必传的,而且数据类型必须匹配,除非使用?标记为可选参数。语法如下:

function buildName(firstName: string, lastName?: string) {
  if (lastName)
    return firstName + " " + lastName;
  else
    return firstName;
}

这里firstName必传,不传会报错。而lastName是可选的,因为它带有?

其次,函数参数也可以指定默认值,但指定了默认值的参数不允许设为可选:

function cal(price:number,rate:number = 0.50) { 
    var discount = price * rate; 
    console.log("计算结果: ",discount); 
} 

这是因为设置了默认值的参数必定是有值的,因此它不可能是可选参数。

最后,所有的可选参数必须位于参数列表的后面。这是因为函数参数在赋值时是按序的,写在前面的参数总会先被赋值,因此可能不存在的变量一定位于后面。

5. 接口(interface)

接口定义了一个具有特定结构的对象原型。它以抽象的形式描述了某类对象应该具有的属性和方法,任何继承了该接口的对象都必须实现接口所描述的所有的属性和方法。举例如下:

interface IPerson { 
    firstName:string, 
    lastName:string, 
    sayHi: ()=>string 
} 
 
var customer:IPerson = { 
    firstName:"Tom",
    lastName:"Hanks", 
    sayHi: ():string =>{return "Hi there"} 
} 

接口IPerson描述了一类数据结构,它具有属性firstNamelastName和方法sayHi,但是没有指定它们的值。

现在我们定义了一个变量customer,customer:IPerson表明它实现了IPerson接口,所以它需要实现接口所指定的属性和方法(否则编译会报错)。

在TypeScript中,实现某个接口的对象,必须具有和接口完全一致的属性和方法,不可以新增任何其他属性或方法。如果需要这样做,通过继承来实现:

interface IParent {
  name: string,
  age: number,
}

interface IChildren extends IParent {
  sayHi: ():string => { return 'Hi'; }
}

一个接口可以同时继承多个接口,这称为多继承,它将同时获得这些接口所定义的属性和方法:

interface IParent1 { 
    v1:number 
} 
 
interface IParent2 { 
    v2:number 
} 
 
interface Child extends IParent1, IParent2 { ... } 

6. 类(class)

实际上ES6的语法中已经引入了类的概念,尽管我们知道,JavaScript的类的实现与java是不一样的,但是基本语法却有很多相似之处。

一个类可以包含以下三类成员:

  1. 字段。也就是属性,包括实例属性和静态属性。
  2. 构造函数。构造this所用的方法。
  3. 方法。定义对象支持的操作。

一个简单的类如下:

class Car { 
    // 字段 
    engine:string; 
    static className:string, // 静态属性
    // 构造函数 
    constructor(engine:string) { 
        this.engine = engine 
    }  
    // 方法 
    disp():void { 
        console.log("发动机为 :   "+this.engine) 
    } 
}

let car = new Car('v8');

除了特殊的类型声明外,这与es6的class没有太大差别。继承的语法也是类似的,使用extends关键字:

class Benz extends Car {
  ...
}

当然了,像java中一样,类可以用于实现一个接口,也使用implements关键字。实现接口时不要求类的属性和方法和接口保持一致,只要求类必须实现接口所定义的全部属性和方法即可:

interface ILoan { 
   interest:number 
} 
 
class AgriLoan implements ILoan { 
   interest:number 
   rebate:number 
   
   constructor(interest:number,rebate:number) { 
      this.interest = interest 
      this.rebate = rebate 
   } 
} 

TypeScript还参照java的类,为JavaScript引入了访问控制符,即publicprotectedprivate。其中public是默认权限,没有指定类型的属性或方法均为public类型。如:

class Encapsulate { 
   str1:string = "hello" // public类型
   private str2:string = "world" 
}
 
var obj = new Encapsulate() 
console.log(obj.str1)     // 可访问 
console.log(obj.str2)   // 编译错误, str2 是私有的

7. 对象

TypeScript中定义对象的方法和JavaScript是一致的,但是它不允许为一个已定义的对象直接新增属性或方法:

let obj = { name: '张三' };

obj.name = '李四';  
obj.age = 25; // 报错,obj没有age属性

obj在初始时只有name属性,在尝试为其新增age属性时就会报错。如果想要设置它的age属性,应该设置一个初始值来占位:

let obj = { name: '张三', age: -1 };

鸭子类型(Duck Typing)

"当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,
那么这只鸟就可以被称为鸭子。"

比如我们定义了如下接口:

interface IPoint {
  x: number,
  y: number
}

function addPoints(p1:IPoint,p2:IPoint):IPoint { 
    var x = p1.x + p2.x 
    var y = p1.y + p2.y 
    return {x:x,y:y} 
} 
// 正确
var newPoint = addPoints({x:3,y:4},{x:5,y:1})  
// 错误 
var newPoint2 = addPoints({x:1},{x:4,y:3})

在这里,我们传入addPoints的参数并未明确指定为IPoint类型。但是对于TypeScript来说,只要该对象具备number类型的x属性,和number类型的y属性,那么它就可以作为IPoint类型的数据传入函数。

8. 命名空间(namespace)

所谓的命名空间,就是一个相对独立的命名作用域。这个作用域内所定义的变量,与外部的任何变量互不干扰,因此可以解决变量重名问题。

举个例子,假设某学校一年级5班和6班都有一个学生叫“王小明”,在不指定班级的情况下,我么无法知道“王小明”到底指哪个人。现在如果我们带了班级前缀,指明是6班的“王小明”,那么两个人就得以区分了。这里的5班和6班,就是我们所说的两个命名空间。

TypeScript使用namespace来定义一个命名空间:
shape.ts

namespace Shape { 
   export interface IShape {      }  
   export class Rect {      }  
}

namespace Shape2 {
  export interface IShape {      }  
  export class Triangle {      }  
}

我们在同一个文件内定义了两个命名空间:Shape和Shape2,这就好比是我们上面所说的5班和6班。这两个空间内都有名为IShape的接口,它就像我们上面所说的“王小明”同学。

由于命名空间的存在,我们可以很容易区分这两个接口,方法是在使用的时候加上命名空间前缀,如Shape.IShapeShape2.IShape(这就像在称呼“5班的王小明”和“6班的王小明”)。需要注意的是,命名空间内的变量、接口或类必须通过export向外暴露才可以被访问,否则它就只在该命名空间内部可访问。

当然了,在同一个文件里声明两个命名空间并不常见,更多的情况是,将某个ts文件声明为一整个命名空间。不同的ts文件可以声明到同一个命名空间下,这样它们的变量在访问时就不需要加命名空间前缀,但是由于被声明到了同一命名空间,此时变量就不能重名了。

TypeScript使用如下的三斜线语法引入其他ts文件,这种语法相当于把被依赖的文件直接复制到当前文件:
Color.ts

/// <reference path="Shape.ts"/>
namespace Color {
  // Rect是Shape命名空间的类,需要加命名空间前缀才能调用
  let rect = new Shape.Rect();
  ...
}

9. 模块

TypeScript的模块系统与CommonJS、AMD以及ES6的模块系统没有明显的差异,也是使用import、export和require这几个关键字进行模块的导入和导出。

TypeScript的模块语法更像是CommonJS和ES6模块系统的糅合产物,它使用如下的export语法向外暴露接口(这是借鉴自ES6模块语法):
IShape.ts

export interface IShape { 
   draw(); 
}

然后在另一个文件中通过以下语法引入上述接口:
Circle.ts

import shape = require('./IShape.ts');
export class Circle implements shape.IShape { 
   public draw() { 
      console.log("Cirlce is drawn (external module)"); 
   } 
}

这里的导入语法类似于ES6模块语法和CommonJS的混合语法:

// ES6语法
import IShape from './IShape.js';
// CommonJS语法
let IShape = require('./IShape.js');

TypeScript相当于隐式地将模块导出为一个对象,然后将通过export暴露的变量、方法、接口、类等作为属性添加到该对象上,在其他文件中通过import导入的就是这个对象本身。

10. 声明文件

TypeScript的声明文件,类似于C/C++的头文件,主要用于静态类型检查。

在大多数项目中,我们不免会引入一些第三方JavaScript库,如jQuery。但是这些库可能不一定是用TypeScript编写的,所以当引入这样的库时,TypeScript就无法直接对其进行类型检查,甚至在进行类型检查时还可能产生编译报错。如:

let btn = $("#btn");  // 报错,因为当前文件并未定义$

上面的代码会产生编译报错,因为TypeScript在编译上述文件时根本不知道$的数据类型是什么,甚至不知道它是否存在。

引用一个用TypeScript重新实现的jQuery的确可以解决上述问题,但第三方JavaScript库的数量至少数以百万计,每一个库都可能遇到这个问题,对其逐一改造实在不现实。为此,TypeScript提供了声明文件来解决这个问题。

声明文件以.d.ts结尾,仅用于声明某些没有指明类型的变量、函数、类等,比如上面的jQuery的例子:
jquery.d.ts

declare var $: (selector: string) => any;
declare var jQuery: (selector: string) => any;

我们在声明文件里通过declare关键字手动定义了变量$jQuery的类型,它们都是接收一个字符串作为参数,返回值为任意类型的函数。我们在自己的ts文件中直接引入上述声明文件:

/// <refrence path="./jquery.d.ts"/>
let btn = $('#btn');

现在编译就不会报错了,因为我们已经在声明文件里告诉TypeScript编译器,$jQuery是一个接收一个字符串参数的函数了,TypeScript检查发现我们的调用符合该规则时就认为该语句没有问题,从而编译通过。

主流的第三方JavaScript库都有现成的声明文件,我们无需手动编写,如jquery的声明文件,运行npm install @types/jquery即可安装。凡是由declare声明的变量、函数等,在编译完后都会被去除,它们仅用于帮助TypeScript识别未指明数据类型的标识符。

总结

TypeScript的设计初衷是为JavaScript添加静态语言的语法特性,使之能适应大型项目的开发。从目前的形势来看,前端开发者学习TypeScript已是势在必行。

与TypeScript类似的还有Facebook的Flow,但目前已经处于濒死状态(毕竟连Facebook自己的项目都选择用TypeScript,而不是自家的Flow;Vue 2.x也是采用Flow,后来尤大神也在知乎承认是自己当初看走了眼,因此3.0版本已经完全改为TypeScript实现),已经没有学习的必要。

本文只是TypeScript的一些基础语法,关于TypeScript在项目中的使用,还需要更多的经验积累,以后再行补充。

猜你喜欢

转载自blog.csdn.net/qq_41694291/article/details/107144955