TypeScript
什么是 TypeScript
简介
加强版的 JavaScript,添加了类型系统
JavaScript 是一门弱类型语言,它弱化了对一切变量的类型,是一门非常灵活的编程语言
- 它没有类型约束,一个变量可能初始化时是字符串,过一会儿又被赋值为数字。
- 由于隐式类型转换的存在,有的变量的类型很难在运行前就确定。
- 基于原型的面向对象编程,使得原型上的属性或方法可以在运行时被修改。
- 函数是 JavaScript 中的一等公民,可以赋值给变量,也可以当作参数或返回值。
而 TypeScript 的类型系统,在很大程度上弥补了 JavaScript 的缺点
类型系统按照「类型检查的时机」来分类,可以分为动态类型和静态类型,TypeScript 是静态类型
动态类型与静态类型
动态类型是指在运行时才会进行类型检查,这种语言的类型错误往往会导致运行时错误,JavaScript 是一门解释性语言,没有编译阶段,所以它是动态类型。
/**
* 动态类型 JavaScript
*/
const num = 10;
num.push(2);
/**
* 动态类型 JavaScript
*/
const num = 10;
num.push(2);
上面的代码 num 是一个数字,但是却调用了数组的 push 方法,但是编译器并不会提示你这个错误,只有再运行这个文件时告诉你 num.push is not a function
**TypeScript 是一个静态类型。**指编译阶段就能确定每个变量的类型,这种语言的类型错误往往会导致语法错误。TypeScript 在运行前需要先编译为 JavaScript,而在编译阶段就会进行类型检查
/**
* 静态类型 TypeScript
*/
const num = 10;
num.push(2);
/**
* 静态类型 TypeScript
*/
const num = 10;
num.push(2);
上面的代码,使用 TypeScript 时,再编译器就会提示你 类型“10”上不存在属性“push”
。
强类型和弱类型
类型系统按照「是否允许隐式类型转换」来分类,可以分为强类型和弱类型。
以下这段代码不管是在 JavaScript 中还是在 TypeScript 中都是可以正常运行的,运行时数字 1
会被隐式类型转换为字符串 '1'
,加号 +
被识别为字符串拼接,所以打印出结果是字符串 '11'
。
console.log(1 + "1");
// 打印出字符串 '11'
console.log(1 + "1");
// 打印出字符串 '11'
TypeScript 是完全兼容 JavaScript 的,它不会修改 JavaScript 运行时的特性,所以它们都是弱类型。
安装 TypeScript
安装:npm install -g typescript
检查版本号:tsc -v
运行 tsc xx.ts
文件自动生成 对应的 JS 文件,自动编译成 JS 代码。
注意: 在 vscode 中用 tsc 编译 ts 文件的时候报错,tsc : 无法加载文件,因为在此系统上禁止运行脚本;SecurityError 解决方法
安装 ts-node
使用 ts-node 插件更加方便,有了这个插件。就可以少了上面一步编译到 js 文件。直接输出 ts 文件结果了。
安装:npm install -g ts-node
使用: ts-node xx.ts
原始数据类型
数据类型分为:原始数据类型 Primitive Data Types 和 对象类型 Object Types
主要介绍较为常见的五种基础数据类型: 布尔值、数值、字符串、null、undefined
/**
* 布尔值
*/
const isDone: boolean = true;
/**
* 布尔值
*/
const isDone: boolean = true;
/**
* 数字
*/
const isNumber: number = 10;
/**
* 数字
*/
const isNumber: number = 10;
/**
* 字符串
*/
const myName: string = "Bob";
/**
* 字符串
*/
const myName: string = "Bob";
Null 和 Undefined
在 TypeScript 中,可以使用 null
和 undefined
来定义这两个原始数据类型
let u: undefined = undefined;
let n: null = null;
let u: undefined = undefined;
let n: null = null;
任意类型 Any
用来表示允许赋值为任意类型
当一个变量被声明为 any 类型,那么它就与在 JavaScript 的变量一样不会有类型的约束。那么其实给变量赋值为 any 类型其实就已经失去了 TypeScript 对类型的约束的意义了。由此不太建议经常使用。
/**
* 任意值 any
*/
const anyThing: any = "I am any type";
// 这里编辑器并不会提示你 anyThing 是否有该属性或方法
console.log(anyThing.myName);
console.log(anyThing.test());
/**
* 任意值 any
*/
const anyThing: any = "I am any type";
// 这里编辑器并不会提示你 anyThing 是否有该属性或方法
console.log(anyThing.myName);
console.log(anyThing.test());
类型推论
如果没有明确指定变量的类型,那么 TypeScript 就会自动去推断变量的类型。
注意:定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查
/**
* 类型推论
*/
let myFavoriteNumber = "seven";
// 提示你:不能将类型“number”分配给类型“string”
myFavoriteNumber = 7;
/**
* 类型推论
*/
let myFavoriteNumber = "seven";
// 提示你:不能将类型“number”分配给类型“string”
myFavoriteNumber = 7;
联合类型
表示可以定义多种类型,使用 |
分隔每种类型
/**
* 联合类型
*/
// 此时 favoriteNumber 可以被赋值为 string 或者是 number 类型
const favoriteNumber: string | number = 7;
/**
* 联合类型
*/
// 此时 favoriteNumber 可以被赋值为 string 或者是 number 类型
const favoriteNumber: string | number = 7;
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法
function getLength(something: string | number): string {
// TS会提示你 类型“string | number”上不存在属性“length”。类型“number”上不存在属性“length”
return something.length;
// 此时我们只能访问到 .toLocaleString() .toString() .valueOf() 方法,因为这在string 和 number都存在
}
function getLength(something: string | number): string {
// TS会提示你 类型“string | number”上不存在属性“length”。类型“number”上不存在属性“length”
return something.length;
// 此时我们只能访问到 .toLocaleString() .toString() .valueOf() 方法,因为这在string 和 number都存在
}
联合类型的变量在被赋值的时候,会根据类型推论的规则自动推断出其类型
/**
* 联合类型
*/
const favoriteNumber: string | number = 78.45678;
// 自动推断出为联合类型的 number 类型
console.log(favoriteNumber.toFixed(2));
// 那么之后去调用 number 没有的方法或属性就会报错
console.log(favoriteNumber.length);
/**
* 联合类型
*/
const favoriteNumber: string | number = 78.45678;
// 自动推断出为联合类型的 number 类型
console.log(favoriteNumber.toFixed(2));
// 那么之后去调用 number 没有的方法或属性就会报错
console.log(favoriteNumber.length);
对象类型-接口 interface
在 TypeScript 中,我们可以使用接口来定义对象中属性的各个类型。在 TypeScript 里接口的作用就是为这些类型命名和为代码或第三方代码定义契约。
什么是接口
在面向对象语言中,例如 Java,接口是一个很重要的概念,它是对行为的抽象,而具体如何去实现这种行为接口并不需要考虑,需要由类 class 去 实现 implement。
而 TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对 对象的形状(Shape)进行描述与定义。
简单的 Demo
接口定义的属性或方法,变量在指定了其类型为该接口,就必须去实现 接口里定义的契约,不可多有或少有出接口没有的属性或方法。
/**
* 人
*/
interface IPerson {
name: string;
age: number;
}
// 这时我们 有一个对象 需要去实现 IPerson 这个接口
const tom: IPerson = {
name: "Tom",
age: 21,
};
/**
* 人
*/
interface IPerson {
name: string;
age: number;
}
// 这时我们 有一个对象 需要去实现 IPerson 这个接口
const tom: IPerson = {
name: "Tom",
age: 21,
};
interface-可选属性
IPerson 中有个 sex 属性,属于可选属性。在实现接口时可定义也可不定义其属性。
interface IPerson {
name: string;
age: number;
sex?: number; // 可选属性
}
// 这时我们 有一个 对象 需要去实现 IPerson 这个接口
const tom: IPerson = {
name: "Tom",
age: 21,
};
interface IPerson {
name: string;
age: number;
sex?: number; // 可选属性
}
// 这时我们 有一个 对象 需要去实现 IPerson 这个接口
const tom: IPerson = {
name: "Tom",
age: 21,
};
interface-任意属性
有时候我们希望一个接口允许有任意的属性,可以使用如下方式
prop 被定义为 string 类型,可以返回 any 类型,此时在实现该接口时 在定义 name、age 属性,sex 属性为可选属性之后,用户可以随意定义接口中没有的属性。
interface IPerson {
name: string;
age: number;
sex?: number; // 可选属性
[prop: string]: any; // 任意属性
}
// 这时我们 有一个 对象 需要去实现 IPerson 这个接口
const tom: IPerson = {
name: "Tom",
age: 21,
gender: "male",
};
interface IPerson {
name: string;
age: number;
sex?: number; // 可选属性
[prop: string]: any; // 任意属性
}
// 这时我们 有一个 对象 需要去实现 IPerson 这个接口
const tom: IPerson = {
name: "Tom",
age: 21,
gender: "male",
};
需要注意的是,一旦定义了任意属性返回的类型而不是 any,那么确定属性和可选属性的类型都必须是它的类型的子集
interface IPerson {
name: string;
age: number;
sex?: number; // 可选属性
[prop: string]: string; // 任意属性
}
// 这时我们 有一个 对象 需要去实现 IPerson 这个接口
const tom: IPerson = {
name: "Tom",
age: 21,
gender: "male",
};
interface IPerson {
name: string;
age: number;
sex?: number; // 可选属性
[prop: string]: string; // 任意属性
}
// 这时我们 有一个 对象 需要去实现 IPerson 这个接口
const tom: IPerson = {
name: "Tom",
age: 21,
gender: "male",
};
上面的例子,任意属性返回值为 string 类型,此时编辑器会报错提示你:
不能将类型“{ name: string; age: number; gender: string; }”分配给类型“IPerson”。属性“age”与索引签名不兼容。不能将类型“number”分配给类型“string”
interface-只读属性
有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly
定义只读属性。之后去修改只读属性就会报错。
interface IPerson {
readonly id: number; // 只读属性
name: string;
age: number;
sex?: number; // 可选属性
[prop: string]: any; // 任意属性
}
// 这时我们 有一个 对象 需要去实现 IPerson 这个接口
const tom: IPerson = {
id: 20211027,
name: "Tom",
age: 21,
gender: "male",
};
interface IPerson {
readonly id: number; // 只读属性
name: string;
age: number;
sex?: number; // 可选属性
[prop: string]: any; // 任意属性
}
// 这时我们 有一个 对象 需要去实现 IPerson 这个接口
const tom: IPerson = {
id: 20211027,
name: "Tom",
age: 21,
gender: "male",
};
数组的类型
在 TypeScript 中,数组类型可以有多种定义方法,较为灵活
类型[] 表示法
// 类型[] 表示法
const numberArray: number[] = [1, 3, 5, 7, 9];
// 类型[] 表示法
const numberArray: number[] = [1, 3, 5, 7, 9];
数组泛型
也可以使用数组泛型 Array<elemType>
表示数组
// 数组泛型
const numberArray2: Array<number> = [1, 3, 5, 7];
// 数组泛型
const numberArray2: Array<number> = [1, 3, 5, 7];
用接口描述数组
INumberArray
表示:只要索引的类型是数字时,那么值的类型必须是数字。
虽然接口也可以用来描述数组,但是我们一般不会这么做,因为这种方式比前两种方式复杂多了。
// 用接口表示数组
interface INumberArray {
[index: number]: number;
}
const numberArray3: INumberArray = [1, 3, 5, 7, 9, 11];
// 用接口表示数组
interface INumberArray {
[index: number]: number;
}
const numberArray3: INumberArray = [1, 3, 5, 7, 9, 11];
函数的类型
函数的声明
在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression)
/**
* Fuction Delaration
*/
function sum(x, y) {
return x + y;
}
/**
* Function Expression
*/
let sumFunction = function (x, y) {
return x + y;
};
/**
* Fuction Delaration
*/
function sum(x, y) {
return x + y;
}
/**
* Function Expression
*/
let sumFunction = function (x, y) {
return x + y;
};
一个函数有输入或输出,在 TS 中对其输入和输出进行类型上的约束,需要把输入和输出都考虑到。
函数声明的类型定义方式如下:
/**
* Fuction Delaration
*/
function sum(x: number, y: number): number {
return x + y;
}
/**
* Fuction Delaration
*/
function sum(x: number, y: number): number {
return x + y;
}
函数表达式的类型定义方式如下:
/**
* Function Expression
*/
let sumFunction = function (x: number, y: number): number {
return x + y;
};
/**
* Function Expression
*/
let sumFunction = function (x: number, y: number): number {
return x + y;
};
这是可以通过编译的,不过事实上,上面的代码只对等号右侧的匿名函数进行了类型定义,而等号左边的 sumFunction
,是通过赋值操作进行类型推论而推断出来的。如果需要我们手动给 mySum
添加类型,则应该是这样
/**
* Function Expression
*/
let sumFunction: (x: number, y: number) => number = function (
x: number,
y: number
): number {
return x + y;
};
/**
* Function Expression
*/
let sumFunction: (x: number, y: number) => number = function (
x: number,
y: number
): number {
return x + y;
};
用接口来定义函数
/**
* 用接口来定义函数
*/
interface ISearchFunc {
(source: string, subString: string): boolean;
}
const mySearch: ISearchFunc = function (
source: string,
subString: string
): boolean {
return source.search(subString) !== -1;
};
// boolean
console.log(typeof mySearch("test", "te"));
/**
* 用接口来定义函数
*/
interface ISearchFunc {
(source: string, subString: string): boolean;
}
const mySearch: ISearchFunc = function (
source: string,
subString: string
): boolean {
return source.search(subString) !== -1;
};
// boolean
console.log(typeof mySearch("test", "te"));
采用函数表达式|接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。
可选参数
与接口中的可选属性类似,我们用 ?
表示可选的参数,需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了。
/**
* 可选参数
*/
function buildName(firstName: string, lastName?: string): string {
if (lastName) {
return firstName + " " + lastName;
}
return firstName;
}
/**
* 可选参数
*/
function buildName(firstName: string, lastName?: string): string {
if (lastName) {
return firstName + " " + lastName;
}
return firstName;
}
参数默认值
在 ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数,此时就不受「可选参数必须接在必需参数后面」的限制了。
/**
* 参数默认值
*/
function buildName(firstName: string, lastName: string = "Cat"): string {
if (lastName) {
return firstName + " " + lastName;
}
return firstName;
}
// This is Cat
console.log(buildName("This is"));
/**
* 参数默认值
*/
function buildName(firstName: string, lastName: string = "Cat"): string {
if (lastName) {
return firstName + " " + lastName;
}
return firstName;
}
// This is Cat
console.log(buildName("This is"));
剩余参数
可以使用 ...rest
的方式获取函数中的剩余参数(rest 参数),restOfName 是一个数组
/**
* 剩余参数
*/
function restName(
firstName: string,
secondName: string,
...restOfName: string[]
) {
// [ 'Lucas', 'MacKinzie' ]
console.log("restOfName", restOfName);
return firstName + " " + " " + secondName + restOfName.join(" ");
}
let employeeName = restName("Joseph", "Samuel", "Lucas", "MacKinzie");
/**
* 剩余参数
*/
function restName(
firstName: string,
secondName: string,
...restOfName: string[]
) {
// [ 'Lucas', 'MacKinzie' ]
console.log("restOfName", restOfName);
return firstName + " " + " " + secondName + restOfName.join(" ");
}
let employeeName = restName("Joseph", "Samuel", "Lucas", "MacKinzie");
重载
重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。类似 Java 当中的方法重载。
- 函数重载
场景是老师要查询学生的用户信息,如果是输入数字就通过排名(id)去匹配,输入的是字符串就通过分数(grades)去匹配。如果查询相同分数太多,老师输入 string 想添加一个变量 count 用于控制查询数量。
function getUserInfo(value:number):User|undefined
function getUserInfo(value:string,count:number):User[]
function getUserInfo(value:number|string,count:number=1):User|User[]|undefined{
if(typeof value==='number'){
return userList.find(item=>item.id===value)
}else{
return userList.filter(item=>item.grades===value).slice(0,count)
}
}
getUserInfo('98',3)
function getUserInfo(value:number):User|undefined
function getUserInfo(value:string,count:number):User[]
function getUserInfo(value:number|string,count:number=1):User|User[]|undefined{
if(typeof value==='number'){
return userList.find(item=>item.id===value)
}else{
return userList.filter(item=>item.grades===value).slice(0,count)
}
}
getUserInfo('98',3)
- 方法重载
应用例子:简单封装一个数组,使数组更加好用,通过 index 删除返回 index,通过 object 删除返回 object。
class ArrayEN {
constructor(public arr: object[]) {}
get(Index: number) {
return this.arr[Index];
}
delete(value: number): number;
delete(value: object): object;
delete(value: number | object): number | object {
this.arr = this.arr.filter((item, index) => {
if (typeof value === "number") {
return value !== index;
} else {
return value !== item;
}
});
return value;
}
}
class ArrayEN {
constructor(public arr: object[]) {}
get(Index: number) {
return this.arr[Index];
}
delete(value: number): number;
delete(value: object): object;
delete(value: number | object): number | object {
this.arr = this.arr.filter((item, index) => {
if (typeof value === "number") {
return value !== index;
} else {
return value !== item;
}
});
return value;
}
}
类型断言
通过 类型断言 可以告诉编辑器: 相信我,我知道自己在干什么。类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript 会假设你,程序员,已经进行了必须的检查。
// 第一种: 尖括号 语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
console.log("strLength", strLength);
// 第二种: as 语法
let someValue2: any = "this is a string";
let strLength2: number = (someValue2 as string).length;
console.log("strLength2", strLength2);
// 第一种: 尖括号 语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
console.log("strLength", strLength);
// 第二种: as 语法
let someValue2: any = "this is a string";
let strLength2: number = (someValue2 as string).length;
console.log("strLength2", strLength2);
声明文件
什么是声明文件?
通常我们会把声明语句放到一个单独的文件(xxx.d.ts
)中,这就是声明文件,声明文件必需以 .d.ts
为后缀。一般来说,ts 会解析项目中所有的 *.ts
文件,当然也包含以 .d.ts
结尾的文件。所以当我们将 xxx.d.ts
放到项目中时,其他所有 *.ts
文件就都可以获得 jQuery
的类型定义了。
声明全局变量
declare var/let/const xxx
// src/jQuery.d.ts
declare let jQuery: (selector: string) => any;
// src/jQuery.d.ts
declare let jQuery: (selector: string) => any;
一般来说,全局变量都是禁止修改的常量,所以大部分情况都应该使用 const
而不是 var
或 let
。
声明全局函数
函数类型的声明语句中,函数重载也是支持的
declare function globalTest(selector: string): string;
declare function globalTest(sekector2: number): number;
declare function globalTest(selector: string): string;
declare function globalTest(sekector2: number): number;
声明全局类
declare class Animal {
name: string;
constructor(name: string);
sayHi(): string;
}
declare class Animal {
name: string;
constructor(name: string);
sayHi(): string;
}
更多声明文件内容参考 TS 入门教程:https://ts.xcatliu.com/basics/declaration-files.html 更全面
类型别名
类型别名用来给一个类型起个新名字。使用 type
创建类型别名,常用于联合类型。
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === "string") {
return n;
} else {
return n();
}
}
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === "string") {
return n;
} else {
return n();
}
}
字符串字面量类型
适合用来约束取值只能是某个字符串的其中一个。注意,类型别名与字符串字面量类型都是使用 type
进行定义。
type EventNames = "Click" | "Scroll" | "MouseMove";
function handleEvent(event: EventNames) {
console.log(event);
}
// 取值只能为 "Click" | "Scroll" | "MouseMove" 其中一个
handleEvent("Scroll");
type EventNames = "Click" | "Scroll" | "MouseMove";
function handleEvent(event: EventNames) {
console.log(event);
}
// 取值只能为 "Click" | "Scroll" | "MouseMove" 其中一个
handleEvent("Scroll");
元组
数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。
let tom: [string, number] = ["Tom", 25];
let tom: [string, number] = ["Tom", 25];
枚举
枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。使用 enum
关键字来定义。
枚举成员默认会被赋值为从 0
开始递增的数字,同时也会对枚举值到枚举名进行反向映射:
enum Days {
Sun,
Mon,
Tue,
Wed,
Thu,
Fri,
Sat,
}
console.log(Days["Sun"] === 0); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true
console.log(Days[0] === "Sun"); // true
console.log(Days[1] === "Mon"); // true
console.log(Days[2] === "Tue"); // true
console.log(Days[6] === "Sat"); // true
enum Days {
Sun,
Mon,
Tue,
Wed,
Thu,
Fri,
Sat,
}
console.log(Days["Sun"] === 0); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true
console.log(Days[0] === "Sun"); // true
console.log(Days[1] === "Mon"); // true
console.log(Days[2] === "Tue"); // true
console.log(Days[6] === "Sat"); // true
手动赋值
手动赋值的枚举项会接着上一个枚举项递增。
enum Days {
Sun = 7,
Mon = 1,
Tue,
Wed,
Thu,
Fri,
Sat,
}
console.log(Days["Sun"] === 7); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true
enum Days {
Sun = 7,
Mon = 1,
Tue,
Wed,
Thu,
Fri,
Sat,
}
console.log(Days["Sun"] === 7); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true
字符串枚举
enum Person {
name = "NAME",
age = "AGE",
love = "LOVE",
hobby = "HOBBY",
}
console.log(Person.name); // NAME
console.log(Person.hobby); // HOBBY
enum Person {
name = "NAME",
age = "AGE",
love = "LOVE",
hobby = "HOBBY",
}
console.log(Person.name); // NAME
console.log(Person.hobby); // HOBBY
异构枚举
enum Person {
name = 1,
age = 2,
love = "LOVE",
hobby = "HOBBY",
}
console.log(Person.name); // 1
console.log(Person.hobby); // HOBBY
enum Person {
name = 1,
age = 2,
love = "LOVE",
hobby = "HOBBY",
}
console.log(Person.name); // 1
console.log(Person.hobby); // HOBBY
常量枚举
常量枚举通过在枚举上使用
const
修饰符来定义,常量枚举不同于常规的枚举,他们会在编译阶段被删除。
const enum Size {
WIDTH = 10,
HEIGHT = 20,
}
const area = Size.WIDTH * Size.HEIGHT;
// 常量枚举成员在使用的地方会被内联进来,之所以可以这么做是因为,常量枚举不允许包含计算成员;如上例所示,在运行时是没有 Size 变量的,因此常量枚举会带来一个对性能的提升。
const enum Size {
WIDTH = 10,
HEIGHT = 20,
}
const area = Size.WIDTH * Size.HEIGHT;
// 常量枚举成员在使用的地方会被内联进来,之所以可以这么做是因为,常量枚举不允许包含计算成员;如上例所示,在运行时是没有 Size 变量的,因此常量枚举会带来一个对性能的提升。
类
简单的类例子:
class Greeter {
greeting: string; // 属性
constructor(message: string) {
// 构造函数
this.greeting = message;
}
greet() {
// 方法
return "Hello," + this.greeting;
}
}
let greeter = new Greeter("world");
console.log(greeter); // Greeter { greeting: 'world' }
console.log(greeter.greeting); // world
console.log(greeter.greet()); // Hello,world
class Greeter {
greeting: string; // 属性
constructor(message: string) {
// 构造函数
this.greeting = message;
}
greet() {
// 方法
return "Hello," + this.greeting;
}
}
let greeter = new Greeter("world");
console.log(greeter); // Greeter { greeting: 'world' }
console.log(greeter.greeting); // world
console.log(greeter.greet()); // Hello,world
继承
注意点:
- 子类的构造函数 必须调用
super()
它会去执行 父类的构造函数。 - 并且在构造函数里一定是先执行
super()
在可以访问this
属性
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(); // Woof! Woof!
dog.move(10); // Animal moved 10m.
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(); // Woof! Woof!
dog.move(10); // Animal moved 10m.
class Animal {
// Animal类
name: string;
constructor(theName: string) {
this.name = theName;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
// Snake 类
constructor(name: string) {
super(name);
}
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters); // 调用 Animal类 中的 move方法
}
}
class Horse extends Animal {
// Horse 类
constructor(name: string) {
super(name);
}
move(distanceInMeters = 45) {
console.log("Galloping...");
super.move(distanceInMeters); // 调用 Animal类 中的 move方法
}
}
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.
/**
* Snake类和 Horse类都创建了 move方法,它们重写了从 Animal继承来的 move方法,使得 move方法根据不同的类而具有不同的功能。
* 即使 tom被声明为 Animal类型,但因为它的值是 Horse,调用 tom.move(34)时,它会调用 Horse里重写的方法
*/
class Animal {
// Animal类
name: string;
constructor(theName: string) {
this.name = theName;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
// Snake 类
constructor(name: string) {
super(name);
}
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters); // 调用 Animal类 中的 move方法
}
}
class Horse extends Animal {
// Horse 类
constructor(name: string) {
super(name);
}
move(distanceInMeters = 45) {
console.log("Galloping...");
super.move(distanceInMeters); // 调用 Animal类 中的 move方法
}
}
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.
/**
* Snake类和 Horse类都创建了 move方法,它们重写了从 Animal继承来的 move方法,使得 move方法根据不同的类而具有不同的功能。
* 即使 tom被声明为 Animal类型,但因为它的值是 Horse,调用 tom.move(34)时,它会调用 Horse里重写的方法
*/
公共、私有和受保护的修饰符
public 公共
在 TS 中,成员默认为 public。 所以可以默认不写
private 私有
当成员被标记成 private 时, 该成员就不可以在声明它的类 的外部访问,只能内部访问。
class Animal {
private name: string;
constructor(theName: string) {
this.name = theName;
}
}
new Animal("Cat").name; // 编辑器提示:属性“name”为私有属性,只能在类“Animal”中访问
class Animal {
private name: string;
constructor(theName: string) {
this.name = theName;
}
}
new Animal("Cat").name; // 编辑器提示:属性“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()); // Hello, my name is Howard and I work in Sales.
console.log(howard.name); // 编辑器提示:属性“name”受保护,只能在类“Person”及其子类中访问
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()); // Hello, my name is Howard and I work in Sales.
console.log(howard.name); // 编辑器提示:属性“name”受保护,只能在类“Person”及其子类中访问
readonly 只读
属性设置为只读属性。只读属性必须在声明是或构造函数里被初始化
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor(theNmae: string) {
this.name = theName;
}
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // 错误! name 是只读的.
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor(theNmae: string) {
this.name = theName;
}
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // 错误! name 是只读的.
Getters 和 Setters
TS 通过 getters/setters 来控制对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。
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"; // 会走类的 set fullName() {} 方法
if (employee.fullName) {
console.log(employee.fullName);
}
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"; // 会走类的 set fullName() {} 方法
if (employee.fullName) {
console.log(employee.fullName);
}
静态属性
可以创建类的静态成员, 静态属性只存在类本身上而不是 new 出来的实例上。
class Grid {
static origin = { x: 0, y: 0 };
scale: number;
calculateDistanceFromOrigin(point: { x: number; y: number }) {
let xDist = point.x - Grid.origin.x;
let yDist = point.y - Grid.origin.y;
return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
}
constructor(scale: number) {
this.scale = scale;
}
}
let grid1 = new Grid(1.0); // 1x scale
let grid2 = new Grid(5.0); // 5x scale
console.log(grid1.calculateDistanceFromOrigin({ x: 10, y: 10 }));
// 14.142135623730951
console.log(grid2.calculateDistanceFromOrigin({ x: 10, y: 10 }));
// 2.8284271247461903
class Grid {
static origin = { x: 0, y: 0 };
scale: number;
calculateDistanceFromOrigin(point: { x: number; y: number }) {
let xDist = point.x - Grid.origin.x;
let yDist = point.y - Grid.origin.y;
return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
}
constructor(scale: number) {
this.scale = scale;
}
}
let grid1 = new Grid(1.0); // 1x scale
let grid2 = new Grid(5.0); // 5x scale
console.log(grid1.calculateDistanceFromOrigin({ x: 10, y: 10 }));
// 14.142135623730951
console.log(grid2.calculateDistanceFromOrigin({ x: 10, y: 10 }));
// 2.8284271247461903
抽象类
抽象类是作为其它派生类的基类使用。 它们不会直接被实例化,不同于接口,抽象类可以包含成员的实现细节。
abstract
关键字是用于定义抽象类 和 定义抽象类内部的抽象方法
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log("roaming the earch...");
}
}
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log("roaming the earch...");
}
}
注意点:
- 抽象类的抽象方法 在抽象类中不包含具体的实现 但是 必须在派生类中实现。
- 抽象方法的语法与接口方法相似。 两者都是定义方法签名但不包含方法体。 然而,抽象方法必须包含
abstract
关键字并且可以包含访问修饰符。
abstract class Department {
constructor(public name: string) {}
printName(): void {
console.log("Department name:" + this.name);
}
abstract printMeeting(): void; // 必须在派生类中实现
}
class AccountingDepartment extends Department {
constructor() {
super("Accounting and Auditing"); // 派生类必须在构造函数中调用 super()
}
printMeeting(): void {
console.log("The Accounting Department meets each Monday at 10am.");
}
generateReports(): void {
console.log("Generating accounting reports...");
}
}
abstract class Department {
constructor(public name: string) {}
printName(): void {
console.log("Department name:" + this.name);
}
abstract printMeeting(): void; // 必须在派生类中实现
}
class AccountingDepartment extends Department {
constructor() {
super("Accounting and Auditing"); // 派生类必须在构造函数中调用 super()
}
printMeeting(): void {
console.log("The Accounting Department meets each Monday at 10am.");
}
generateReports(): void {
console.log("Generating accounting reports...");
}
}
类实现接口
通过接口也可以实现对类的属性或方法进行某种契约。
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 ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) {}
}
接口继承接口
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square: Square = {
color: "red",
sideLength: 1,
};
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square: Square = {
color: "red",
sideLength: 1,
};
泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
function createArray(length: number, value: any): Array<any> {
let result = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
console.log(createArray(5, "A"));
function createArray(length: number, value: any): Array<any> {
let result = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
console.log(createArray(5, "A"));
这段代码编译不会报错,但是一个显而易见的缺陷是,它并没有准确的定义返回值的类型:
Array<any>
允许数组的每一项都为任意类型。但是我们预期的是,数组中每一项都应该是输入的 value
的类型。
这时候,泛型就派上用场了:
function createArray<T>(length: number, value: T): Array<T> {
let result = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
console.log(createArray<string>(5, "A"));
// 也可以不指定,通过类型推论也可以自动推算出来
console.log(createArray(5, "A"));
function createArray<T>(length: number, value: T): Array<T> {
let result = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
console.log(createArray<string>(5, "A"));
// 也可以不指定,通过类型推论也可以自动推算出来
console.log(createArray(5, "A"));
泛型约束
在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法
function loggingIdentiy<T>(arg: T): T {
// 编辑器提示:类型“T”上不存在属性“length”
console.log(arg.length);
return arg;
}
function loggingIdentiy<T>(arg: T): T {
// 编辑器提示:类型“T”上不存在属性“length”
console.log(arg.length);
return arg;
}
上例中,泛型 T
不一定包含属性 length
,所以编译的时候报错了。
这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 length
属性的变量。这就是泛型约束
// string 类型中有 length 属性
function loggingIdentiy<T extends string>(arg: T): T {
console.log(arg.length);
return arg;
}
// 或者是继承相关的接口
interface ILength {
length: number;
}
function loggingIdentiy<T extends ILength>(arg: T): T {
console.log(arg.length);
return arg;
}
// string 类型中有 length 属性
function loggingIdentiy<T extends string>(arg: T): T {
console.log(arg.length);
return arg;
}
// 或者是继承相关的接口
interface ILength {
length: number;
}
function loggingIdentiy<T extends ILength>(arg: T): T {
console.log(arg.length);
return arg;
}
泛型函数接口
interface ConfigFn {
<T>(value: T): T;
}
var getData: ConfigFn = (value) => {
return value;
};
console.log(getData<string>("张三"));
// 提示:类型“number”的参数不能赋给类型“string”的参数
console.log(getData<string>(1));
interface ConfigFn {
<T>(value: T): T;
}
var getData: ConfigFn = (value) => {
return value;
};
console.log(getData<string>("张三"));
// 提示:类型“number”的参数不能赋给类型“string”的参数
console.log(getData<string>(1));
泛型类
class GetMin<T> {
arr: T[] = [];
add(ele: T) {
this.arr.push(ele);
}
min(): T {
var min = this.arr[0];
this.arr.forEach((value) => {
if (value < min) {
min = value;
}
});
return min;
}
}
var gm1 = new GetMin<number>();
gm1.add(5);
gm1.add(2);
gm1.add(3);
gm1.add(1);
console.log(gm1.min()); // 1
var gm2 = new GetMin<string>();
gm2.add("tom");
gm2.add("jerry");
gm2.add("jack");
gm2.add("sunny");
console.log(gm2.min()); // jack
class GetMin<T> {
arr: T[] = [];
add(ele: T) {
this.arr.push(ele);
}
min(): T {
var min = this.arr[0];
this.arr.forEach((value) => {
if (value < min) {
min = value;
}
});
return min;
}
}
var gm1 = new GetMin<number>();
gm1.add(5);
gm1.add(2);
gm1.add(3);
gm1.add(1);
console.log(gm1.min()); // 1
var gm2 = new GetMin<string>();
gm2.add("tom");
gm2.add("jerry");
gm2.add("jack");
gm2.add("sunny");
console.log(gm2.min()); // jack
泛型参数的默认类型
在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用
function createArray<T = string>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
function createArray<T = string>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}