TypeScript 初体验

TypeScript👌

前言:

原由是公司大佬要求给其余同事培训ts,而后我本身在网上东粘粘西粘粘的,虽然不是我本身写成的,可是也是我认真作的备课,因为某些缘由也没能排上用场,因此决定上传,这里若是大佬们看到有相同的地方,还请谅解。🌹前端

why use TypeScript?

JavaScript是一种弱类型的语言, 且JavaScript具备动态类型检查的特征。程序员

强类型和弱类型

强类型语言是指当一个变量一旦被指定了数据类型以后,若是不通过强制转换,那么他那么它就永远都是这个类型的了。ajax

弱类型语言是指一个变量能够被赋值不一样类型的数据。typescript

动态类型和静态类型

静态类型语言和动态类型语言得核心区别在于,静态类型语言(statically-typed languages)会在编译时(compile time)进行类型检查,而动态语言(dynamically-typed)则是在运行时进行类型检查(runtime)npm

当出现一个类型错误时,静态类型检查和动态类型检查的差别就凸显出来了。在静态类型语言中,类型检查发生在编译阶段。在动态类型语言中,只有在程序运行了一次的时候错误才会被发现,也就是在运行时。编程

动态类型存在的问题

因为JavaScript是动态类型语言,没有编译的环节,全部的类型检查是在代码运行时进行的,这就意味着咱们书写的代码中的某些错误,只能在代码运行时在会被发现。json

好比下面这段代码:数组

function greet(o){
  // 这句代码,咱们尝试去调用了obj的sayHello方法
  // 在编码阶段,咱们没法肯定最终传进来的参数对象到底是什么
  // 只有在代码实际运行阶段,执行到本句代码的时候
  // 才会发现传入的obj对象根本没有sayHello方法,最终在运行阶段报错
  o.sayHello();
}

var obj = {
  name: '张学友'
}

greet(obj); // Uncaught TypeError: o.sayHello is not a function
复制代码

再看下面这段代码:浏览器

/** * 计算指定数值除以2的结果 咱们指望用户传入的参数为数值类型 * @param {number} x */
function divideBy2(x) {
  return x / 2;
}

// 用户在调用的时候若是传入数值,则功能正常
var res = divideBy2(4);  // 2
// 可是若是用户在调用的时候传入一个字符串,则会致使运行时错误
// (不报错,由于JavaScript解释器会尝试纠正错误,可是结果确定不是咱们预期的)
var res1 = divideBy2('Hello World'); // NaN
复制代码

固然,动态类型带来的并不仅是问题,它的灵活程度和编码成本相较于静态类型的语言来说是显而易见的。好比在Java和C#被类型限制的生活不能自理的同窗,在JavaScript中你几乎能够放飞自我,随心所欲(Just a joke)。bash

静态类型带来的好处

你能够尽早发现bug和错误

静态类型检查容许咱们在程序没有运行以前就能够肯定咱们所设定的肯定性是不是对的。一旦有违反这些既定规则的行为,它能在运行以前就发现,而不是在运行时。

由于类型检查器会在你编码的时候就告诉你错误,因此这也就比你把代码交付到客户手中才发现一些错误要更方面(或者说付出更少的开发与维护成本)。

提升代码的可读性

在代码中加入类型系统,能够清晰的告诉用户功能所须要的数据是什么类型的,函数的返回值是什么类型的,提高代码的可读性。

减小了复杂的错误处理逻辑

假设咱们须要提供一个函数用来计算数组中全部数字的和

// 最基本的代码以下:
let  sum = arr => {
  let result = 0;
  arr.forEach(v => {
    result += v;
  })
  return result;
}

// 但是上面的代码对于可能出现的异常没有作任何的处理
// 为了保证函数可以正常的运行,咱们须要确保用户传入的参数为有效的数字数组
// 那么就须要这么作
let  sum = arr => {
  if(!arr){
    throw new Error("Please give me arguments");
  }

  if(!Array.isArray(arr)){
    throw new Error("I need Array, what you've passed to me?");
  }

  if(!arr.every(v => typeof v == 'number')){
    throw new Error("你传进来的数组里有奇怪的东西,我要的是数字!")
  }

  let result = 0;
  arr.forEach(v => {
    result += v;
  })
  return result;
}
复制代码

如此咱们便发现,若是没有类型系统,要处理相似的问题,代码显得很是繁琐。

当有了类型系统以后,这样代码就不须要再写了,在咱们学习完flowtypescript以后咱们回过头来再看这个例子。

促进更可靠的重构

假设要进行代码重构,咱们须要将函数的某个参数进行修改,那么在以前修改的时候咱们可能须要犹豫,由于指不定项目中某个地方调用没有进行修改,那么运行的时候会产生奇怪的问题。

而有了静态类型检测以后,类型检测会自动告诉咱们修改后的代码哪里存在问题,咱们只须要按照提示修复便可。

加强IDE的功能

静态类型会加强IDE的功能,提高开发效率。

静态类型存在的问题

  1. 会增长代码量
  2. 须要花时间掌握类型
  3. 可能会下降开发效率

如何在JavaScript开发中使用静态类型

  1. Flow: FaceBook的开源技术
  2. TypeScript: 微软公司开发的语言

ts是什么?

TypeScript是由微软公司开发的一个开源JavaScript的超集,主要提供了类型系统和对ES6的支持,他能够编译成纯 JavaScript. 任何现有的 JavaScript 都是合法的 TypeScript 程序。

TypeScript从出现至今已经成为了前端领域中不可忽视的技术,各大流行框架都已经支持使用 TypeScript 做为开发语言。

  • TypeScript是微软公司开发的一款开源的JavaScript超集语言!
  • JavaScript超集: 当前任何JavaScript都是合法的TypeScript代码!
  • TypeScript主要为JavaScript提供了类型系统和ES6语法的支持!
  • Flow是一个类型检查工具,TypeScript是一种开发语言!
  • TypeScript有本身的编译工具,咱们写好的TypeScript代码最终会经过编译器编译成JavaScript代码进行运行!

安装 TypeScript

TypeScript 最终要运行起来,咱们须要将 TypeScript 代码转换成对应的 JavaScript 代码,那么 TypeScript 的命令行工具就能够帮咱们完成这件事情。

TypeScript 的命令行工具安装方法以下:

npm install -g typescript
复制代码

以上命令会在全局环境下安装 tsc 命令,安装完成以后,咱们就能够在任何地方执行 tsc 命令了。

编译一个 TypeScript 文件很简单:

tsc hello.ts
复制代码

咱们约定使用 TypeScript 编写的文件以 .ts 为后缀

ts配置文件的说明

  1. 建立配置文件
tsc --init
复制代码
  1. 设置配置项

    • target: 指的就是将ts代码要转换成哪一个版本的js代码 es5 es3
    • module: 指的就是将ts代码转换成js代码以后,使用的模块化的标准是什么
    • outDir: 指的就是将ts代码转换成js代码以后,js代码存放的文件夹路径
    • rootDir: 指的就是要将哪一个目录中的ts代码进型转换,ts代码的存放路径
    • strict: 是否要将ts代码转换为严格模式的js代码!
  2. 使用配置文件

    tsc -p ./tsconfig.json
    复制代码

TypeScript 初体验

接下来咱们书写一段代码,你们不须要纠结详细的技术点,只须要看 TypeScript 给咱们带来的功能体验便可。

function greeter(msg: string) {
  return "Hello, " + msg;
}

let str = "World";
console.log(greeter(str)); // Hello,World
复制代码

上面这段代码就是一段 TypeScript 代码,咱们在这段代码中规定了函数greeter传入的参数必须类型是string。咱们将这段代码保存为greeter.ts文件

接下来咱们在命令行中执行

tsc greeter.ts
复制代码

这个命令会将咱们写好的 ts 文件转换成相应的 JavaScript 代码文件

function greeter(msg) {
  return "Hello, " + msg;
}
var str = "World";
console.log(greeter(msg));
复制代码

上述例子中,咱们用 : 指定 greeter参数类型为 string。可是编译为 js 以后,并无什么检查的代码被插入进来。

TypeScript 只会进行静态检查,若是发现有错误,编译的时候就会报错

下面尝试把这段代码编译一下:

function greeter(msg: string) {
  return "Hello, " + msg;
}

let str = [0, 1, 2];
console.log(greeter(str));
复制代码

编辑器中会提示错误,编译的时候也会出错:

index.ts(6,22): error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'string'.
复制代码

可是仍是生成了 js 文件:

function greeter(msg) {
  return "Hello, " + msg;
}

let str = [0, 1, 2];
console.log(greeter(str));
复制代码

TypeScript 编译的时候即便报错了,仍是会生成编译结果,咱们仍然可使用这个编译以后的文件。

TypeScript 数据类型

介绍

为了让程序有价值,咱们须要可以处理最简单的数据单元:数字,字符串,结构体,布尔值等。 TypeScript 支持与 JavaScript 几乎相同的数据类型,此外还提供了实用的枚举类型方便咱们使用。

布尔值

最基本的数据类型就是简单的 true/false 值,在 JavaScript 和 TypeScript 里叫作boolean(其它语言中也同样)。

let isDone: boolean = false;
复制代码

数字

和 JavaScript 同样,TypeScript 里的全部数字都是浮点数。 这些浮点数的类型是number。 除了支持十进制和十六进制字面量,TypeScript 还支持 ECMAScript 2015 中引入的二进制和八进制字面量。

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d; // 十六进制
let binaryLiteral: number = 0b1010; // 二进制
let octalLiteral: number = 0o744; // 八进制
复制代码

字符串

JavaScript 程序的另外一项基本操做是处理网页或服务器端的文本数据。 像其它语言里同样,咱们使用string表示文本数据类型。 和 JavaScript 同样,可使用双引号(")或单引号(')表示字符串。

let name: string = "bob";
name = "smith";
复制代码

你还可使用模版字符串,它能够定义多行文本和内嵌表达式。 这种字符串是被反引号包围(```),而且以${ expr }这种形式嵌入表达式

let name: string = `Gene`;
let age: number = 37;
let sentence: string = `Hello, my name is ${name}. I'll be ${age + 1} years old next month.`;
复制代码

这与下面定义sentence的方式效果相同:

let sentence: string =
  "Hello, my name is " +
  name +
  ".\n\n" +
  "I'll be " +
  (age + 1) +
  " years old next month.";
复制代码

数组

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

let list: number[] = [1, 2, 3];
复制代码

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

let list: Array<number> = [1, 2, 3];
复制代码

元组 Tuple

元组类型容许表示一个已知元素数量和类型的数组,各元素的类型没必要相同。 好比,你能够定义一对值分别为stringnumber类型的元组。

// 声明一个元祖类型
let x: [string, number];
// Initialize it
x = ["hello", 10]; // OK
// Initialize it incorrectly
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"; // error

console.log(x[5].toString()); // error

x[6] = true; // Error, 布尔不是(string | number)类型
复制代码

联合类型是高级主题,咱们会在之后的章节里讨论它。

枚举

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

enum Color {
  Red,
  Green,
  Blue
}
let c: Color = Color.Green; // 1
复制代码

默认状况下,从0开始为元素编号。 你也能够手动的指定成员的数值。 例如,咱们将上面的例子改为从1开始编号:

enum Color {
  Red = 1,
  Green,
  Blue
}
let c: Color = Color.Green;// 2
复制代码

或者,所有都采用手动赋值:

enum Color {
  Red = 1,
  Green = 2,
  Blue = 4
}
let c: Color = Color.Green;// 2
复制代码

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

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

alert(colorName); // 显示'Green'由于上面代码里它的值是2
复制代码

任意值 Any

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

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
复制代码

在对现有代码进行改写的时候,any类型是十分有用的,它容许你在编译时可选择地包含或移除类型检查。 你可能认为Object有类似的做用,就像它在其它语言中那样。 可是Object类型的变量只是容许你给它赋任意值 - 可是却不可以在它上面调用任意的方法,即使它真的有这些方法:

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

let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.
复制代码

当你只知道一部分数据的类型时,any类型也是有用的。 好比,你有一个数组,它包含了不一样的类型的数据:

let list: any[] = [1, true, "free"];

list[1] = 100;
复制代码

Emmm...就是什么类型都行,当你没法确认在处理什么类型时能够用这个。

但要慎重使用,用多了就失去使用Ts的意义。

主要应用场景有:

  1. 接入第三方库
  2. Ts菜逼前期都用:joy:

空值 Void

某种程度上来讲,void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你一般会见到其返回值类型是void

function warnUser(): void {
  alert("This is my warning message");
  // 注意 这个时候 咱们不能使用return
}
复制代码

Typescript中,你必须在函数中定义返回类型。像这样:

若没有返回值,则会报错:

咱们能够将其返回值定义为void:

此时将没法 return

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

let unusable: void = undefined;
复制代码

Null 和 Undefined

TypeScript 里,undefinednull二者各自有本身的类型分别叫作undefinednull。 和void类似,它们的自己的类型用处不是很大:

// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;
复制代码

默认状况下nullundefined是全部类型的子类型。 就是说你能够把nullundefined赋值给number类型的变量。

然而,当你指定了--strictNullChecks标记,nullundefined只能赋值给void和它们各自。 这能避免不少常见的问题。 也许在某处你想传入一个stringnullundefined,你可使用联合类型string | null | undefined。 再次说明,稍后咱们会介绍联合类型。

注意:咱们鼓励尽量地使用--strictNullChecks,但在本手册里咱们假设这个标记是关闭的。

Never

never类型表示的是那些永不存在的值的类型。 例如,never类型是那些老是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也多是never类型,当它们被永不为真的类型保护所约束时。

never类型是任何类型的子类型,也能够赋值给任何类型;然而,没有类型是never的子类型或能够赋值给never类型(除了never自己以外)。 即便any也不能够赋值给never

下面是一些返回never类型的函数:

// 返回never的函数必须存在没法达到的终点
function error(message: string): never {
  throw new Error(message);
}

// 推断的返回值类型为never
function fail() {
  return error("Something failed");
}

// 返回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
create("string"); // Error
create(false); // Error
create(undefined); // Error
复制代码

类型断言

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

经过类型断言这种方式能够告诉编译器,“相信我,我知道本身在干什么”。 类型断言比如其它语言里的类型转换,可是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起做用。 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语法断言是被容许的。

关于let

你可能已经注意到了,咱们使用let关键字来代替你们所熟悉的 JavaScript 关键字varlet关键字是 JavaScript 的一个新概念,TypeScript 实现了它。 咱们会在之后详细介绍它,不少常见的问题均可以经过使用let来解决,因此尽量地使用let来代替var吧。

介绍

传统的 JavaScript 程序使用函数和基于原型的继承来建立可重用的组件,但对于熟悉使用面向对象方式的程序员来说就有些棘手,由于他们用的是基于类的继承而且对象是由类构建出来的。 从 ECMAScript 2015,也就是 ECMAScript 6 开始,JavaScript 程序员将可以使用基于类的面向对象的方式。 使用 TypeScript,咱们容许开发者如今就使用这些特性,而且编译后的 JavaScript 能够在全部主流浏览器和平台上运行,而不须要等到下个 JavaScript 版本。

下面看一个使用类的例子:

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

let greeter = new Greeter("world");
复制代码

若是你使用过 C#或 Java,你会对这种语法很是熟悉。 咱们声明一个Greeter类。这个类有 3 个成员:一个叫作greeting的属性,一个构造函数和一个greet方法。

你会注意到,咱们在引用任何一个类成员的时候都用了this。 它表示咱们访问的是类的成员。

最后一行,咱们使用new构造了Greeter类的一个实例。 它会调用以前定义的构造函数,建立一个Greeter类型的新对象,并执行构造函数初始化它。

简单示例:

class person {
    // 和ES6不一样的是, ts 中属性必须声明, 须要指定类型
    name: string;
    // 声明好属性以后, 属性必须赋值一个默认值或者在构造函数中进行初始化
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
    sayHello(msg: string): void {
        console.log(msg);
    }
}

let xm = new person('小明', 18);
xm.sayHello('你们好');
复制代码

继承

在 TypeScript 里,咱们可使用经常使用的面向对象模式。 基于类的程序设计中一种最基本的模式是容许使用继承来扩展示有的类。

看下面的例子:

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是一个派生类,它派生自Animal基类,经过extends关键字。 派生类一般被称做子类,基类一般被称做超类

由于Dog继承了Animal的功能,所以咱们能够建立一个Dog的实例,它可以bark()move()

下面咱们来看个更加复杂的例子。

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

class Snake extends Animal {
  constructor(name: string) {
    super(name);
  }
  move(distanceInMeters = 5) {
    console.log("Slithering...");
    super.move(distanceInMeters);
  }
}

class Horse extends Animal {
  constructor(name: string) {
    super(name);
  }
  move(distanceInMeters = 45) {
    console.log("Galloping...");
    super.move(distanceInMeters);
  }
}

let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");

sam.move();
// Slithering...
// Sammy the Python moved 5m.
tom.move(34);
// Galloping...
// Tommy the Palomino moved 34m.
复制代码

这个例子展现了一些上面没有提到的特性。 这一次,咱们使用extends关键字建立了Animal的两个子类:HorseSnake

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

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

简单示例:

class Animal {
    age: number;
    constructor(age: number) {
        this.age = age;
    }
    eat() {
        console.log('吃个大鸡腿');
    }
}

class Dog extends Animal{
    type: string;
    constructor(type: string, age: number) {
        // 要使用super
        super(age);
        this.type = type;
    }
    // 若是子类中出现了和父类相同名字的方法,则会进行覆盖
    // 也就是调用的时候, 调用的是子类中的方法了!
    eat () {
        console.log('我是狗对象中的eat方法');
    }
}
let dog = new Dog('哈士奇', 2);
dog.eat();

复制代码

公共,私有与受保护的修饰符

指的就是能够在类的成员前经过添加关键字来设置当前成员的访问权限

  • public: 公开的,默认 全部人均可以进行访问
  • private: 私有的, 只能在当前类中进行访问
  • protected: 受保护的,这能在当前类或者子类中进行访问

默认为public

在上面的例子里,咱们能够自由的访问程序里定义的成员。 若是你对其它语言中的类比较了解,就会注意到咱们在以前的代码里并无使用public来作修饰;例如,C#要求必须明确地使用public指定成员是可见的。 在 TypeScript 里,成员都默认为public

你也能够明确的将一个成员标记成public。 咱们能够用下面的方式来重写上面的Animal类:

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

理解private

当成员被标记成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 不兼容.
复制代码

这个例子中有AnimalRhino两个类,RhinoAnimal类的子类。 还有一个Employee类,其类型看上去与Animal是相同的。 咱们建立了几个这些类的实例,并相互赋值来看看会发生什么。 由于AnimalRhino共享了来自Animal里的私有成员定义private name: string,所以它们是兼容的。 然而Employee却不是这样。当把Employee赋值给Animal的时候,获得一个错误,说它们的类型不兼容。 尽管Employee里也有一个私有成员name,但它明显不是Animal里面定义的那个。

理解protected

protected修饰符与private修饰符的行为很类似,但有一点不一样,protected成员在派生类中仍然能够访问。例如:

class Person {
  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); // 错误
复制代码

注意,咱们不能在Person类外使用name,可是咱们仍然能够经过Employee类的实例方法访问,由于Employee是由Person派生而来的。

构造函数也能够被标记成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' 的构造函数是被保护的.
复制代码

简单示例:

// ts 中类成员的访问修饰符
// ts 中的修饰符指的就是能够在类的成员前经过添加关键字来设置当前成员的访问权限

// public: 公开的, 默认 全部人均可以进行访问
// private: 私有的, 只有在当前类中进行访问
// protected: 受保护的 只能在当前类或者子类中进行访问


enum Colors {
    red,
    yellow,
    blue
}

class Car {
    public color: Colors;
    constructor() {
        this.color = Colors.red
        this.run();
        this.loadPeople();
    }
    private run () {

    }
    protected loadPeople () {

    }
}

let aoDi = new Car();
// aoDi.color;
aoDi.color;
// aoDi.run();
// aoDi.loadPeople();

class Byd extends Car {
    sayHi() {
        this.loadPeople();
        // this.run(); // error
        console.log(this.color);
    }
}
let bw = new Byd();
bw.color;
bw.sayHi()
// benchi.loadPeople(); error
// benchi.run(); // error
复制代码

readonly 修饰符

你可使用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 是只读的.
复制代码

参数属性

在上面的例子中,咱们不得不定义一个受保护的成员name和一个构造函数参数theNamePerson类里,而且马上将theName的值赋给name。 这种状况常常会遇到。参数属性能够方便地让咱们在一个地方定义并初始化一个成员。 下面的例子是对以前Animal类的修改版,使用了参数属性:

class Animal {
  constructor(private name: string) {}
  move(distanceInMeters: number) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}
复制代码

注意看咱们是如何舍弃了theName,仅在构造函数里使用private name: string参数来建立和初始化name成员。 咱们把声明和赋值合并至一处。

参数属性经过给构造函数参数添加一个访问限定符来声明。 使用private限定一个参数属性会声明并初始化一个私有成员;对于publicprotected来讲也是同样。

简单示例:

class Cat {
    readonly name: string;
    // type: string
    constructor(public type: string) {
        this.name = '加菲'
        this.type = type
    }
}
let cat = new Cat('橘猫');
// cat.name = 'qqq';

复制代码

存取器

TypeScript 支持经过 getters/setters 来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。

下面来看如何把一个简单的类改写成使用getset。 首先,咱们从一个没有使用存取器的例子开始。

class Employee {
  fullName: string;
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
  console.log(employee.fullName);
}
复制代码

咱们能够随意的设置fullName,这是很是方便的,可是这也可能会带来麻烦。

下面这个版本里,咱们先检查用户密码是否正确,而后再容许其修改员工信息。 咱们把对fullName的直接访问改为了能够检查密码的set方法。 咱们也加了一个get方法,让上面的例子仍然能够工做。

let passcode = "secret passcode";

class Employee {
  private _fullName: string;

  get fullName(): string {
    return this._fullName;
  }

  set fullName(newName: string) {
    if (passcode && passcode == "secret passcode") {
      this._fullName = newName;
    } else {
      console.log("Error: Unauthorized update of employee!");
    }
  }
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
  alert(employee.fullName);
}
复制代码

咱们能够修改一下密码,来验证一下存取器是不是工做的。当密码不对时,会提示咱们没有权限去修改员工。

对于存取器有下面几点须要注意的:

首先,存取器要求你将编译器设置为输出 ECMAScript 5 或更高。 不支持降级到 ECMAScript 3。 其次,只带有get不带有set的存取器自动被推断为readonly

简单示例

// class People {
// name: string = ''
// }
// let p1 = new People();
// p1.name = 'S是的是的沙发地方的';

 
class People {
    private _name:string = "";
    get name(): string {
        return this._name;
    }
    set name(value: string) {
        if (value.length < 2 || value.length > 5) {
            throw new Error('名字不合法,请从新输入');
        }
        this._name = value;
    }
}

let p1 = new People();
p1.name = '是的是的是打算';
console.log(p1.name);
复制代码

接口

介绍

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

接口初探

下面经过一个简单示例来观察接口是如何工做的:

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

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

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

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

interface LabelledValue {
  label: string;
}

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

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

LabelledValue接口就比如一个名字,用来描述上面例子里的要求。 它表明了有一个label属性且类型为string的对象。 须要注意的是,咱们在这里并不能像在其它语言里同样,说传给printLabel的对象实现了这个接口。咱们只会去关注值的外形。 只要传入的对象知足上面提到的必要条件,那么它就是被容许的。

还有一点值得提的是,类型检查器不会去检查属性的顺序,只要相应的属性存在而且类型也是对的就能够。

简单示例:

// 接口
// 接口咱们能够理解为一个约定, 一个规范


// 接口使用 interface 进行声明
interface AjaxOptions {
    url: string,
    type: string,
    data: object,
    success(data: object): void
}
// options参数中 须要包含 url, type, data, success 
function ajax(options: AjaxOptions) {

}
ajax({
    url:'https://www.baidu.com',
    type: 'post',
    data:{},
    success(data) {

    }
})
复制代码

可选属性

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

下面是应用了“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" });
复制代码

带有可选属性的接口与普通的接口定义差很少,只是在可选属性名字定义的后面加一个?符号。

可选属性的好处之一是能够对可能存在的属性进行预约义,好处之二是能够捕获引用了不存在的属性时的错误。 好比,咱们故意将createSquare里的color属性名拼错,就会获得一个错误提示:

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  let newSquare = { color: "white", area: 100 };
  if (config.clor) {
    // Error: Property 'clor' does not exist on type 'SquareConfig'
    newSquare.color = config.clor;
  }
  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 让咱们传入{ size: number; label: string; }到仅指望获得{ label: string; }的函数里。 咱们已经学过了可选属性,而且知道他们在“option bags”模式里颇有用。

然而,天真地将这二者结合的话就会像在 JavaScript 里那样搬起石头砸本身的脚。 好比,拿createSquare例子来讲:

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  // ...
}

let mySquare = createSquare({ colour: "red", width: 100 });
复制代码

注意传入createSquare的参数拼写为colour而不是color。 在 JavaScript 里,这会默默地失败。

你可能会争辩这个程序已经正确地类型化了,由于width属性是兼容的,不存在color属性,并且额外的colour属性是无心义的。

然而,TypeScript 会认为这段代码可能存在 bug。 对象字面量会被特殊对待并且会通过额外属性检查,当将它们赋值给变量或做为参数传递的时候。 若是一个对象字面量存在任何“目标类型”不包含的属性时,你会获得一个错误。

// error: 'colour' not expected in type 'SquareConfig'
let mySquare = createSquare({ colour: "red", width: 100 });
复制代码

绕开这些检查很是简单。 最简便的方法是使用类型断言:

let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
复制代码

然而,最佳的方式是可以添加一个字符串索引签名,前提是你可以肯定这个对象可能具备某些作为特殊用途使用的额外属性。 若是SquareConfig带有上面定义的类型的colorwidth属性,而且还会带有任意数量的其它属性,那么咱们能够这样定义它:

interface SquareConfig {
  color?: string;
  width?: number;
  [propName: string]: any;
}
复制代码

咱们稍后会讲到索引签名,但在这咱们要表示的是SquareConfig能够有任意数量的属性,而且只要它们不是colorwidth,那么就无所谓它们的类型是什么。

还有最后一种跳过这些检查的方式,这可能会让你感到惊讶,它就是将这个对象赋值给一个另外一个变量: 由于squareOptions不会通过额外属性检查,因此编译器不会报错。

let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
复制代码

要留意,在像上面同样的简单代码里,你可能不该该去绕开这些检查。 对于包含方法和内部状态的复杂对象字面量来说,你可能须要使用这些技巧,可是大部额外属性检查错误是真正的 bug。 就是说你遇到了额外类型检查出的错误,好比“option bags”,你应该去审查一下你的类型声明。 在这里,若是支持传入colorcolour属性到createSquare,你应该修改SquareConfig定义来体现出这一点。

简单示例:

// 只读属性
 interface Poin {
    readonly x: number, // 只读属性
    y: number,
    [propName: string]: any // 额外的类型检查
 }
 let pos: Poin = {
     x: 10,
     y: 20,
     z: 40
 }

// pos.x = 40; // error

复制代码

函数类型

接口可以描述 JavaScript 中对象拥有的各类各样的外形。 除了描述带有属性的普通对象外,接口也能够描述函数类型。

为了使用接口表示函数类型,咱们须要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每一个参数都须要名字和类型。

interface SearchFunc {
  (source: string, subString: string): boolean;
}
复制代码

这样定义后,咱们能够像使用其它接口同样使用这个函数类型的接口。 下例展现了如何建立一个函数类型的变量,并将一个同类型的函数赋值给这个变量。

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
  let result = source.search(subString);
  return result > -1;
};
复制代码

对于函数类型的类型检查来讲,函数的参数名不须要与接口里定义的名字相匹配。 好比,咱们使用下面的代码重写上面的例子:

let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
  let result = src.search(sub);
  return result > -1;
};
复制代码

函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。 若是你不想指定类型,TypeScript 的类型系统会推断出参数类型,由于函数直接赋值给了SearchFunc类型变量。 函数的返回值类型是经过其返回值推断出来的(此例是falsetrue)。 若是让这个函数返回数字或字符串,类型检查器会警告咱们函数的返回值类型与SearchFunc接口中的定义不匹配。

let mySearch: SearchFunc;
mySearch = function(src, sub) {
  let result = src.search(sub);
  return result > -1;
};
复制代码

简单示例:

interface SumInterface{
    (a: number, b: number): number
}
let sum: SumInterface = function (a: number, b: number) {
    return a + b ;
}
复制代码

类类型

实现接口

与 C#或 Java 里接口的基本做用同样,TypeScript 也可以用它来明确的强制一个类去符合某种契约。

interface ClockInterface {
  currentTime: Date;
}

class Clock implements ClockInterface {
  currentTime: Date;
  constructor(h: number, m: number) {}
}
复制代码

你也能够在接口中描述一个方法,在类里实现它,如同下面的setTime方法同样:

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 PersonInterface {
    name: string,
    age: number,
    eat(): void
}

// 使用 implements 关键字
class XiaoMing implements PersonInterface {
    name: string = '小明';
    age: number = 19;
    eat() {

    }
}
复制代码

继承接口

和类同样,接口也能够相互继承。 这让咱们可以从一个接口里复制成员到另外一个接口里,能够更灵活地将接口分割到可重用的模块里。

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 TwoPoint {
    x: number,
    y: number
}
interface ThreePoint {
    z: number
}
interface FourPoint extends ThreePoint, TwoPoint {
    time: Date
}

let poi2: FourPoint = {
    x: 100,
    y: 200,
    z: 300,
    time: new Date()
}
复制代码

接口继承类

当接口继承了一个类类型时,它会继承类的成员但不包括其实现。 就好像接口声明了全部类中存在的成员,但并无提供具体实现同样。 接口一样会继承到类的 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() {}
}

// Error: Property 'state' is missing in type 'Image'.
class Image implements SelectableControl {
  select() {}
}

class Location {}
复制代码

在上面的例子里,SelectableControl包含了Control的全部成员,包括私有成员state。 由于state是私有成员,因此只可以是Control的子类们才能实现SelectableControl接口。 由于只有Control的子类才可以拥有一个声明于Control的私有成员state,这对私有成员的兼容性是必需的。

Control类内部,是容许经过SelectableControl的实例来访问私有成员state的。 实际上,SelectableControl就像Control同样,并拥有一个select方法。 ButtonTextBox类是SelectableControl的子类(由于它们都继承自Control并有select方法),但ImageLocation类并非这样的。

// 接口继承类
class Bird {
    type: string = '画眉鸟';
    fly():void {

    }
}
interface Fly extends Bird {

}
let flyingBrid: Fly = {
    type: '啄木鸟',
    fly(): void {

    }
}


// 接口继承类

class Bird {
    type: string;
    eat():void {

    }
}

interface Fly extends Bird {
    fly():void
}

let flyBird: Fly = {
    type: '黄鹂',
    eat(): void{

    },
    fly(): void {

    }
}
复制代码

泛型

泛型:Generics

软件工程的一个主要部分就是构建组件,构建的组件不只须要具备明确的定义和统一的接口,同时也须要组件可复用。支持现有的数据类型和未来添加的数据类型的组件为大型软件系统的开发过程提供很好的灵活性。

C#Java中,可使用"泛型"来建立可复用的组件,而且组件可支持多种数据类型。这样即可以让用户根据本身的数据类型来使用组件。

1. 泛型方法

在TypeScript里,声明泛型方法有如下两种方式:

function gen_func1<T>(arg: T): T {
    return arg;
}
// 或者
let gen_func2: <T>(arg: T) => T = function (arg) {
    return arg;
}
// gen_func2:<T>(arg: T) => T = arg => arg
复制代码

调用方式也有两种:

gen_func1<string>('Hello world');
gen_func2('Hello world'); 
// 第二种调用方式可省略类型参数,由于编译器会根据传入参数来自动识别对应的类型。
复制代码

2. 泛型与Any

Ts 的特殊类型 Any 在具体使用时,能够代替任意类型,咋一看二者好像没啥区别,其实否则:

// 方法一:带有any参数的方法
function any_func(arg: any): any {
    console.log(arg.length);
		return arg;
}

// 方法二:
function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length return arg; } // 方法二:Array泛型方法 function array_func<T>(arg: Array<T>): Array<T> { console.log(arg.length); return arg; } 复制代码
  • 方法二,打印了arg参数的length属性。由于any能够代替任意类型,因此该方法在传入参数不是数组或者带有length属性对象时,会抛出异常。
  • 方法三, 定义了参数类型是Array的泛型类型,确定会有length属性,因此不会抛出异常。

3. 泛型类型

泛型接口:

interface Generics_interface<T> {
    (arg: T): T;
}
 
function func_demo<T>(arg: T): T {
    return arg;
}

let func1: Generics_interface<number> = func_demo;
func1(123);     // 正确类型的实际参数
func1('123');   // 错误类型的实际参数
复制代码

结束语

你没法决定明天是晴仍是雨,爱你的人是否还能留在身边,你此刻的坚持能换来什么,但你能决定今天有没有准备好雨伞,有没有好好爱人,以及是否足够努力。永远不要只看见前方路途遥远而忘了本身坚持多久才走到这里,今天尽力作的虽然辛苦,但将来发生的都是礼物。 以此共勉 😊

相关文章
相关标签/搜索