内容纲要

  • 了解什么是 TypeScript
  • TypeScript 基本语法

1515492554669

TypeScript 介绍

TypeScript 是什么

type+es6.png

TypeScript 是 JavaScript 的强类型版本。然后在编译期去掉类型和特有语法,生成纯粹的 JavaScript 代码。由于最终在浏览器中运行的仍然是 JavaScript,所以 TypeScript 并不依赖于浏览器的支持,也并不会带来兼容性问题。

TypeScript 是 JavaScript 的超集,这意味着他支持所有的 JavaScript 语法。并在此之上对 JavaScript 添加了一些扩展,如 class / interface / module 等。这样会大大提升代码的可阅读性。

和 JavaScript 若类型不同,TypeScript 这种强类型语言最大的优势在于静态类型检查,可以在代码开发阶段就预知一些低级错误的发生。

  • 一种类似于 JavaScript 的语言,在 JavaScript 的基础之上增加了类型,同时增强了 JavaScript 部分语法功能
  • 遵循 EcmaScript 6 标准规范
  • 由微软开发
  • Angular 2 框架采用 TypeScript 编写
  • 背后有微软和谷歌两大公司的支持
  • TypeScript 可以编译成 JavaScript 从而在支持 JavaScript 的环境中运行
  • TypeScript 和 JavaScript 的关系就好比 less 和 css 的关系

静态类型和动态类型

静态和动态

静态(static):无需运行,根据程序代码就能确定结果。

动态(dynamic):只有运行才能确定结果。

类型:对某个数据所具有的性质进行的描述。如它的结构是怎样的,能进行什么操作。

静态类型:数据拥有类型,且仅有数据拥有类型。

动态类型:数据拥有类型,存放数据的变量、表达式也拥有类型,且类型在编译时是固定的。

下图是静态语言和动态语言的比较:

0b68bbc8b230d957e25493e9e1bfada2_hd.jpg

从表中可见,动态语言和静态语言各有优劣,而TypeScript提供了静态语言强类型支持,同时兼容动态语言弱类型的语法,使用者根据项目需求自由选择。

这种动静结合的特性,目前还没在其他语言见过。

Why TypeScript

  • 从 Angular 2 之后,官方推荐使用 TypeScript 作为开发 Angular 应用的首选语言
  • 遵循 EcmaScript 6
  • 强大的 IDE 支持
    • 类型检查
    • 严谨的语法提示
  • 代码重构
  • 可读性良好

TypeScript 使用场景

  • 大型团队开发
  • Angular 官推语言
  • 其它…

    这里引用知乎上一位开发者对使用推广 TypeScript 的看法:

  1. typescript绝对是好东西,不过推广是有难度的:
  2. 1TS是微软制造,最好的开发工具是VS,想想有些人就激动了(什么vi流,sublime流,macbook流,虽然也能写ts,但你无法跟他们说用vs写有多么好);
  3. 2、即使你告诉他们TS有多好,但是几十人的团队里总有一半以上的人不想学新的东西(当然我没有权利说不学新东西的人应该全部滚动,因为互联网打工的是大爷,想跳槽随便找工作);
  4. 3JSer很多没有学习OOP开发经验(特别是从设计/页面重构转过来的);
  5. 4、很多人接触TS前根本没学过JS,经常有人问“使用TS如何写元素拖拽”这样的问题(那是DOM API好伐,不过你跟初学者很难解释明白);
  6. - 来源:知乎
  7. - 链接:https://www.zhihu.com/question/21879449
  8. - 作者:Fula Li

TypeScript 不仅仅用于开发 Angular 应用

https://www.typescriptlang.org/samples/index.html

  • React
  • Angular
  • Node.js
  • Vue.js
  • WeChat

凡是可以写 JavaScript 的都可以使用 TypeScript。

前置知识

  • EcmaScript 6
  • TypeScript 概念及关系
  • 具有一定的 JavaScript 开发经验
  • 有 Java、C#、C++、C 等静态类型语言使用经验更佳

如何学习 TypeScript

  • 官方文档为准
  • 阅读别人的代码
  • 由于 TypeScript 是兼容 EcmaScript 6 的,所以在开发的时候不需要完全学会 TypeScript 再使用
  • 一个建议是有空就学,会了就用
  • 虽然兼容 EcmaScript 6,但建议既然使用了 TypeScript 就让你的 TypeScript 代码更加 TypeScript,这样才能发挥出 TypeScript 的威力。

相关链接

起步

搭建 TypeScript 开发环境

  • 什么是 compiler?
  • less 编译器:less
  • EcmaScript 6 编译器:babel
  • TypeScript 编译器:typescript
  • 一句话:把 TypeScript 转换为 JavaScript ,浏览器就具有运行了
  • 在线测试编译环境 compiler
  • 本地开发编译环境
  1. npm i -g typescript
  2. # 查看版本号
  3. tsc --version
  4. # 查看使用帮助
  5. tsc --help

编辑器的选择

  • Visual Studio Code
  • Sublime
  • Webstorm

Hello World

新建 greeter.ts 并写入以下内容:

  1. function greeter(person) {
  2. return "Hello, " + person;
  3. }
  4. let user = "Jane User";
  5. document.body.innerHTML = greeter(user);

安装编译器:

  1. npm i -g typescript

编译:

  1. tsc greeter.ts

修改 greeter.ts 文件中的代码,为 greeter 函数的参数 person 加上类型声明 :string

  1. function greeter(person: string) {
  2. return "Hello, " + person;
  3. }
  4. let user = "Jane User";
  5. document.body.innerHTML = greeter(user);

重新编译执行。

让我们继续修改:

  1. function greeter(person: string) {
  2. return "Hello, " + person;
  3. }
  4. let user = [0, 1, 2];
  5. document.body.innerHTML = greeter(user);

重新编译,你将看到如下错误:

  1. error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'string'.

接口(Interface)

  1. interface Person {
  2. firstName: string;
  3. lastName: string;
  4. }
  5. function greeter(person: Person) {
  6. return "Hello, " + person.firstName + " " + person.lastName;
  7. }
  8. let user = { firstName: "Jane", lastName: "User" };
  9. document.body.innerHTML = greeter(user);

类(Class)

  1. class Student {
  2. fullName: string;
  3. constructor(public firstName: string, public middleInitial: string, public lastName: string) {
  4. this.fullName = firstName + " " + middleInitial + " " + lastName;
  5. }
  6. }
  7. interface Person {
  8. firstName: string;
  9. lastName: string;
  10. }
  11. function greeter(person : Person) {
  12. return "Hello, " + person.firstName + " " + person.lastName;
  13. }
  14. let user = new Student("Jane", "M.", "User");
  15. document.body.innerHTML = greeter(user);

变量声明

var

  • 作用域
  • 重复声明

let

  • 块级作用域
  • 在同一个块中不能重复声明

const

  • 声明同时必须赋值
  • 一定声明不可改变
    • 对象可以修改
  • 块级作用域

let vs const

使用最小特权原则,所有变量除了你计划去修改的都应该使用const。 基本原则就是如果一个变量不需要对它写入,那么其它使用这些代码的人也不能够写入它们,并且要思考为什么会需要对这些变量重新赋值。 使用 const也可以让我们更容易的推测数据的流动。

基本数据类型

布尔值

  1. let isDone: boolean = false;

数字

  1. let amount: number = 6;

字符串

  • 类型
  • 模板字符串
    • 支持换行
    • 支持内嵌表达式
  • 和 JavaScript 一样,可以使用双引号,也可以使用单引号,推荐单引号
  1. let nickname: string = '张三';

还可以使用模板字符串(换行 + 嵌入表达式):

  1. let nickname: string = `Gene`;
  2. let age: number = 37;
  3. let sentence: string = `Hello, my nickname is ${ nickname }.
  4. I'll be ${ age + 1 } years old next month.`;

数组

TypeScript像JavaScript一样可以操作数组元素。 有两种方式可以定义数组。 第一种,可以在元素类型后面接上[],表示由此类型元素组成的一个数组:

  1. let list: number[] = [1, 2, 3];

第二种方式是使用数组泛型,Array<元素类型>

  1. let list: Array<number> = [1, 2, 3];

元组

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

  1. // Declare a tuple type
  2. let x: [string, number];
  3. // Initialize it
  4. x = ['hello', 10]; // OK
  5. // Initialize it incorrectly
  6. x = [10, 'hello']; // Error

Object

  • 允许赋任意值
  • 但是不能调用任意方法,即便它真的有
  1. let foo: Object = {
  2. name: 'Jack',
  3. age: 18
  4. }

知道即可,用的很少,没有类型校验和语法提示

Any

有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any类型来标记这些变量:

  1. let notSure: any = 4;
  2. notSure = "maybe a string instead";
  3. notSure = false; // okay, definitely a boolean

Void

void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void

  1. function warnUser(): void {
  2. alert("This is my warning message");
  3. }

声明一个void类型的变量没有什么大用,因为你只能为它赋予undefinednull

  1. let unusable: void = undefined;

Null 和 Undefined

void相似,它们的本身的类型用处不是很大:

  1. // Not much else we can assign to these variables!
  2. let u: undefined = undefined;
  3. let n: null = null;

默认情况下nullundefined是所有类型的子类型。 就是说你可以把 nullundefined赋值给number类型的变量。

然而,当你指定了--strictNullChecks 标记,nullundefined 只能赋值给 void 和它们各自。 这能避免 很多常见的问题。许在某处你想传入一个 stringnullundefined,你可以使用联合类型string | null | undefined

注意:我们推荐尽可能地使用--strictNullChecks ,因为它使你的代码更严谨,可以极大的减少出错的几率。

类型推断

有时候你会遇到这样的情况,你会比TypeScript更了解某个值的详细信息。 通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript会假设你,程序员,已经进行了必须的检查。

类型断言有两种形式。 其一是“尖括号”语法:

  1. let someValue: any = "this is a string";
  2. let strLength: number = (<string>someValue).length;

另一个为as语法:

  1. let someValue: any = "this is a string";
  2. let strLength: number = (someValue as string).length;

两种形式是等价的。 至于使用哪个大多数情况下是凭个人喜好;然而,当你在TypeScript里使用JSX时,只有 as语法断言是被允许的。

其它

  • ReadonlyArray<T> 去除了数组的所有可变方法,确保数组创建后再也不能被修改

接口

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

基本示例

  1. function printLabel(labelledObj: { label: string }) {
  2. console.log(labelledObj.label);
  3. }
  4. let myObj = { size: 10, label: "Size 10 Object" };
  5. printLabel(myObj);

类型检查器会查看printLabel的调用。 printLabel有一个参数,并要求这个对象参数有一个名为label类型为string的属性。 需要注意的是,我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。 然而,有些时候TypeScript却并不会这么宽松,我们下面会稍做讲解。

下面我们重写上面的例子,这次使用接口来描述:必须包含一个label属性且类型为string

  1. interface LabelledValue {
  2. label: string;
  3. }
  4. function printLabel(labelledObj: LabelledValue) {
  5. console.log(labelledObj.label);
  6. }
  7. let myObj = {size: 10, label: "Size 10 Object"};
  8. printLabel(myObj);

可选属性

接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。 可选属性在应用“option bags”模式时很常用,即给函数传入的参数对象中只有部分属性赋值了。

  1. interface SquareConfig {
  2. color?: string;
  3. width?: number;
  4. }
  5. function createSquare(config: SquareConfig): {color: string; area: number} {
  6. let newSquare = {color: "white", area: 100};
  7. if (config.color) {
  8. newSquare.color = config.color;
  9. }
  10. if (config.width) {
  11. newSquare.area = config.width * config.width;
  12. }
  13. return newSquare;
  14. }
  15. let mySquare = createSquare({color: "black"});

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly来指定只读属性:

  1. interface Point {
  2. readonly x: number;
  3. readonly y: number;
  4. }

你可以通过赋值一个对象字面量来构造一个Point。 赋值后, xy再也不能被改变了。

  1. let p1: Point = { x: 10, y: 20 };
  2. p1.x = 5; // error!

readonly vs const

  • 常量使用 const
  • 对象属性使用 readonly

解构赋值

数组解构

  1. let input = [1, 2];
  2. let [first, second] = input;
  3. console.log(first); // outputs 1
  4. console.log(second); // outputs 2

上面的写法等价于:

  1. first = input[0];
  2. second = input[1];

利用解构赋值交换变量:

  1. [first, second] = [second, first];

函数参数解构:

  1. function f ([first, second]: [number, number]) [
  2. console.log(first)
  3. console.log(second)
  4. ]
  5. f(1, 2)

解构剩余参数:

  1. let [first, ...rest] = [1, 2, 3, 4]
  2. console.log(first) // 1
  3. console.log(rest) // [2, 3, 4]

也可以忽略其它参数:

  1. let [first] = [1, 2, 3, 4];
  2. console.log(first); // outputs 1

或者跳过解构:

  1. let [, second, , fourth] = [1, 2, 3, 4]

对象解构

示例一:

  1. let o = {
  2. a: "foo",
  3. b: 12,
  4. c: "bar"
  5. };
  6. let { a, b } = o;

就像数组解构,你可以用没有声明的赋值:

  1. let a: number,
  2. b: number;
  3. ({a, b} = {a: 123, b: 456})
  4. console.log(a, b) // 123 456

你可以在对象里使用 ... 语法创建剩余变量:

  1. let { a, ...passthrough } = o;
  2. let total = passthrough.b + passthrough.c.length;

属性解构重命名

你也可以给属性以不同的名字:

  1. let { a: newName1, b: newName2 } = o;

注意,这里的冒号不是指示类型的。 如果你想指定它的类型, 仍然需要在其后写上完整的模式。

  1. let {a, b}: {a: string, b: number} = o;

默认值

  1. function keepWholeObject(wholeObject: { a: string, b?: number }) {
  2. let { a, b = 1001 } = wholeObject;
  3. }

展开操作符

  • 展开数组
  • 展开对象
    • 不会展开方法

解构赋值用于函数声明

  1. type C = {a: string, b?: number}
  2. function f ({a, b}: C): void {
  3. // ...
  4. }

解构赋值用于加载指定模块成员

基本示例

  1. class Person {
  2. name: string;
  3. age: number;
  4. constructor(name: string, age: number) {
  5. this.name = name;
  6. this.age = age;
  7. }
  8. sayHello() {
  9. console.log(this.name);
  10. }
  11. }
  12. let zs: Person = new Person('张三', 18);

构造函数

继承

  1. class Animal {
  2. move(distanceInMeters: number = 0) {
  3. console.log(`Animal moved ${distanceInMeters}m.`);
  4. }
  5. }
  6. class Dog extends Animal {
  7. bark() {
  8. console.log('Woof! Woof!');
  9. }
  10. }
  11. const dog = new Dog();
  12. dog.bark();
  13. dog.move(10);
  14. dog.bark();

这个例子展示了最基本的继承:类从基类中继承了属性和方法。 这里, Dog是一个 派生类,它派生自 Animal 基类,通过 extends关键字。 派生类通常被称作 子类,基类通常被称作 超类

因为 Dog继承了 Animal的功能,因此我们可以创建一个 Dog的实例,它能够 bark()move()

下面是一个更复杂的例子:

  1. class Animal {
  2. name: string;
  3. constructor(theName: string) { this.name = theName; }
  4. move(distanceInMeters: number = 0) {
  5. console.log(`${this.name} moved ${distanceInMeters}m.`);
  6. }
  7. }
  8. class Snake extends Animal {
  9. constructor(name: string) { super(name); }
  10. move(distanceInMeters = 5) {
  11. console.log("Slithering...");
  12. super.move(distanceInMeters);
  13. }
  14. }
  15. class Horse extends Animal {
  16. constructor(name: string) { super(name); }
  17. move(distanceInMeters = 45) {
  18. console.log("Galloping...");
  19. super.move(distanceInMeters);
  20. }
  21. }
  22. let sam = new Snake("Sammy the Python");
  23. let tom: Animal = new Horse("Tommy the Palomino");
  24. sam.move();
  25. tom.move(34);

与前一个例子的不同点是,派生类包含了一个构造函数,它 必须调用 super(),它会执行基类的构造函数。 而且,在构造函数里访问 this的属性之前,我们 一定要调用 super()。 这个是TypeScript强制执行的一条重要规则。

这个例子演示了如何在子类里可以重写父类的方法。 Snake类和 Horse类都创建了 move方法,它们重写了从Animal继承来的 move方法,使得 move方法根据不同的类而具有不同的功能。 注意,即使 tom被声明为Animal类型,但因为它的值是 Horse,调用 tom.move(34)时,它会调用 Horse里重写的方法:

  1. Slithering...
  2. Sammy the Python moved 5m.
  3. Galloping...
  4. Tommy the Palomino moved 34m.

实例成员访问修饰符

public 开放的

  • 默认为 public
  1. class Animal {
  2. public name: string;
  3. public constructor(theName: string) { this.name = theName; }
  4. public move(distanceInMeters: number) {
  5. console.log(`${this.name} moved ${distanceInMeters}m.`);
  6. }
  7. }

private 私有的

  • 不能被外部访问,只能在类的内部访问使用
  • 私有成员不会被继承
  1. class Person {
  2. public name: string;
  3. public age: number = 18;
  4. private type: string = 'human'
  5. public constructor (name, age) {
  6. this.name = name
  7. this.age = age
  8. }
  9. }

protected 受保护的

  • private 类似,但是可以被继承
  1. class Person {
  2. protected name: string;
  3. constructor(name: string) { this.name = name; }
  4. }
  5. class Employee extends Person {
  6. private department: string;
  7. constructor(name: string, department: string) {
  8. super(name)
  9. this.department = department;
  10. }
  11. public getElevatorPitch() {
  12. return `Hello, my name is ${this.name} and I work in ${this.department}.`;
  13. }
  14. }
  15. let howard = new Employee("Howard", "Sales");
  16. console.log(howard.getElevatorPitch());
  17. console.log(howard.name); // 错误

注意,我们不能在 Person类外使用 name,但是我们仍然可以通过 Employee类的实例方法访问,因为Employee是由 Person派生而来的。

readonly 只读的

在参数中使用修饰符

在上面的例子中,我们不得不定义一个受保护的成员 name和一个构造函数参数 theNamePerson类里,并且立刻给 nametheName赋值。 这种情况经常会遇到。 参数属性可以方便地让我们在一个地方定义并初始化一个成员。

  1. class Person {
  2. name: string;
  3. age: number;
  4. constructor(name: string, age: number) {
  5. this.name = name;
  6. this.age = age;
  7. }
  8. }

可以简写为:

  1. class Person {
  2. constructor(public name: string, public age: number) {
  3. }
  4. }

属性的存(get)取(set)器

  1. let passcode = "secret passcode";
  2. class Employee {
  3. // 私有成员,外部无法访问
  4. private _fullName: string;
  5. // 当访问 实例.fullName 的时候会调用 get 方法
  6. get fullName(): string {
  7. return this._fullName;
  8. }
  9. // 当对 实例.fullName = xxx 赋值的时候会调用 set 方法
  10. set fullName(newName: string) {
  11. if (passcode && passcode == "secret passcode") {
  12. this._fullName = newName;
  13. }
  14. else {
  15. console.log("Error: Unauthorized update of employee!");
  16. }
  17. }
  18. }
  19. let employee = new Employee();
  20. employee.fullName = "Bob Smith";
  21. if (employee.fullName) {
  22. alert(employee.fullName);
  23. }

静态成员

  • 不需要实例化访问的成员称之为静态成员,即只能被类访问的成员
  • static 关键字
  1. class Grid {
  2. static origin = {x: 0, y: 0};
  3. calculateDistanceFromOrigin(point: {x: number; y: number;}) {
  4. let xDist = (point.x - Grid.origin.x);
  5. let yDist = (point.y - Grid.origin.y);
  6. return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
  7. }
  8. constructor (public scale: number) { }
  9. }
  10. let grid1 = new Grid(1.0); // 1x scale
  11. let grid2 = new Grid(5.0); // 5x scale
  12. console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
  13. console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

函数

函数参数

  • 参数及返回值类型
  1. function add(x: number, y: number): number {
  2. return x + y
  3. }
  • 可选参数
  1. function add(x: number, y?: number): number {
  2. return x + 10
  3. }
  • 默认参数
  1. function add(x: number, y: number = 20): number {
  2. return x + y
  3. }
  • 剩余参数
  1. function sum(...args: number[]): number {
  2. let ret: number = 0
  3. args.forEach((item: number): void => {
  4. ret += item
  5. })
  6. return ret
  7. }
  8. sum(1, 2, 3)

箭头函数

  • 基本示例
  1. let add = (x: number, y: number): number => x + y

for-of 循环

  • for 循环
  • forEach
    • 不支持 break
  • for in
    • 会把数组当作对象来遍历
  • for of
    • 支持 break

类型推断(Type Inference)

类型兼容性

模块

概念

模块通信:导出

  1. export default xxx
  2. export const foo: string = 'bar';
  3. export const bar: string = 'foo';

模块通信:导入

  1. // 加载默认成员
  2. import xxx from '模块标识'
  3. // 按需加载模块成员
  4. import {foo, bar} from '模块'

TypeScript 总结

  • TypeScript 是什么
  • 变量声明
    • var
    • let
    • const
  • 基本数据类型
    • 布尔值 boolean
    • 数字 number
    • 字符串 string
    • 数组 number[] 或者 Array<number>
    • 元祖 [number, string]
    • 对象 object ,了解即可
    • 任意类型 any
    • 函数空返回值 void
    • nullundefined
  • 接口
    • interface
  • 解构赋值
    • 数组解构
    • 对象解构
  • 展开操作符
    • 展开数组
    • 展开对象
    • 基本语法
    • 构造函数
    • 继承
    • 属性修饰符
    • 属性的 get 和 set
  • 函数
    • 参数
    • 箭头函数
  • for-of 循环
  • 模块
    • 导出
    • 导入