TypeScript👌
前言:
起因是公司大佬要求给其他同事培训ts,然后我自己在网上东粘粘西粘粘的,虽然不是我自己写成的,但是也是我认真做的备课,由于某些原因也没能排上用场,所以决定上传,这里如果大佬们看到有相同的地方,还请谅解。🌹
why use TypeScript?
JavaScript是一种弱类型的语言, 且JavaScript具有动态类型检查的特征。
强类型和弱类型
强类型语言是指当一个变量一旦被指定了数据类型之后,如果不经过强制转换,那么他那么它就永远都是这个类型的了。
弱类型语言是指一个变量可以被赋值不同类型的数据。
动态类型和静态类型
静态类型语言和动态类型语言得核心区别在于,静态类型语言(statically-typed languages)会在编译时(compile time)进行类型检查,而动态语言(dynamically-typed)则是在运行时进行类型检查(runtime)
当出现一个类型错误时,静态类型检查和动态类型检查的差异就凸显出来了。在静态类型语言中,类型检查发生在编译阶段。在动态类型语言中,只有在程序运行了一次的时候错误才会被发现,也就是在运行时。
动态类型存在的问题
由于JavaScript是动态类型语言,没有编译的环节,所有的类型检查是在代码运行时进行的,这就意味着我们书写的代码中的某些错误,只能在代码运行时在会被发现。
比如下面这段代码:
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)。
静态类型带来的好处
你可以尽早发现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;
}
如此我们便发现,如果没有类型系统,要处理类似的问题,代码显得非常繁琐。
当有了类型系统之后,这样代码就不需要再写了,在我们学习完flow和typescript之后我们回过头来再看这个例子。
促进更可靠的重构
假设要进行代码重构,我们需要将函数的某个参数进行修改,那么在之前修改的时候我们可能需要犹豫,因为指不定项目中某个地方调用没有进行修改,那么运行的时候会产生奇怪的问题。
而有了静态类型检测之后,类型检测会自动告诉我们修改后的代码哪里存在问题,我们只需要按照提示修复即可。
增强IDE的功能
静态类型会增强IDE的功能,提升开发效率。
静态类型存在的问题
- 会增加代码量
- 需要花时间掌握类型
- 可能会降低开发效率
如何在JavaScript开发中使用静态类型
- Flow: FaceBook的开源技术
- 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配置文件的说明
- 创建配置文件
tsc --init
-
设置配置项
- target: 指的就是将ts代码要转换成哪个版本的js代码 es5 es3
- module: 指的就是将ts代码转换成js代码之后,使用的模块化的标准是什么
- outDir: 指的就是将ts代码转换成js代码之后,js代码存放的文件夹路径
- rootDir: 指的就是要将哪个目录中的ts代码进型转换,ts代码的存放路径
- strict: 是否要将ts代码转换为严格模式的js代码!
-
使用配置文件
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
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为string和number类型的元组。
// 声明一个元祖类型
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的意义。
主要应用场景有:
- 接入第三方库
- Ts菜逼前期都用:joy:
空值 Void
某种程度上来说,void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是void:
function warnUser(): void {
alert("This is my warning message");
// 注意 这个时候 我们不能使用return
}
在Typescript中,你必须在函数中定义返回类型。像这样:
若没有返回值,则会报错:
我们可以将其返回值定义为void:
此时将无法 return
声明一个void类型的变量没有什么大用,因为你只能为它赋予undefined和null:
let unusable: void = undefined;
Null 和 Undefined
TypeScript 里,undefined和null两者各自有自己的类型分别叫做undefined和null。 和void相似,它们的本身的类型用处不是很大:
// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;
默认情况下null和undefined是所有类型的子类型。 就是说你可以把null和undefined赋值给number类型的变量。
然而,当你指定了--strictNullChecks标记,null和undefined只能赋值给void和它们各自。 这能避免很多常见的问题。 也许在某处你想传入一个string或null或undefined,你可以使用联合类型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表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型。
使用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 关键字var。 let关键字是 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的两个子类:Horse和Snake。
与前一个例子的不同点是,派生类包含了一个构造函数,它必须调用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 使用的是结构性类型系统。 当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。
然而,当我们比较带有private或protected成员的类型的时候,情况就不同了。 如果其中一个类型里包含一个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 不兼容.
这个例子中有Animal和Rhino两个类,Rhino是Animal类的子类。 还有一个Employee类,其类型看上去与Animal是相同的。 我们创建了几个这些类的实例,并相互赋值来看看会发生什么。 因为Animal和Rhino共享了来自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和一个构造函数参数theName在Person类里,并且立刻将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限定一个参数属性会声明并初始化一个私有成员;对于public和protected来说也是一样。
简单示例:
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 来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。
下面来看如何把一个简单的类改写成使用get和set。 首先,我们从一个没有使用存取器的例子开始。
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。 赋值后,x和y再也不能被改变了。
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带有上面定义的类型的color和width属性,并且还会带有任意数量的其它属性,那么我们可以这样定义它:
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
我们稍后会讲到索引签名,但在这我们要表示的是SquareConfig可以有任意数量的属性,并且只要它们不是color和width,那么就无所谓它们的类型是什么。
还有最后一种跳过这些检查的方式,这可能会让你感到惊讶,它就是将这个对象赋值给一个另一个变量: 因为squareOptions不会经过额外属性检查,所以编译器不会报错。
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
要留意,在像上面一样的简单代码里,你可能不应该去绕开这些检查。 对于包含方法和内部状态的复杂对象字面量来讲,你可能需要使用这些技巧,但是大部额外属性检查错误是真正的 bug。 就是说你遇到了额外类型检查出的错误,比如“option bags”,你应该去审查一下你的类型声明。 在这里,如果支持传入color或colour属性到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类型变量。 函数的返回值类型是通过其返回值推断出来的(此例是false和true)。 如果让这个函数返回数字或字符串,类型检查器会警告我们函数的返回值类型与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方法。 Button和TextBox类是SelectableControl的子类(因为它们都继承自Control并有select方法),但Image和Location类并不是这样的。
// 接口继承类
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'); // 错误类型的实际参数
结束语
你无法决定明天是晴还是雨,爱你的人是否还能留在身边,你此刻的坚持能换来什么,但你能决定今天有没有准备好雨伞,有没有好好爱人,以及是否足够努力。永远不要只看见前方路途遥远而忘了自己坚持多久才走到这里,今天尽力做的虽然辛苦,但未来发生的都是礼物。 以此共勉 😊