TypeScript 的学习
摘要
TypeScript@3.x 的基础学习
# 前言
Q: 为什么学习 TypeScript?
A: 一方面是随着前端的发展,其实越来越偏向于使用 TypeScript 来增加代码的可读性和可维护性。另一方面是因为想要学习更多的东西,更好地去吸收更多的技术。附上 官网 和 教程
# 安装
npm intsall -g TypeScript
安装完成使用 tsc
来进行编译,将 .ts
编译成 .js
let msg: string = "hello world";
console.log(msg);
编译成功为:
var msg = "hello world";
console.log(msg);
- TypeScript 是使用 tsc 进行编译的
- 区分大小写
- 可以不写分号
- 编译器可以忽略注释
- TypeScript 更加强调类型和面向对象
# 类型
# 变量声明
let msg: string = "hello world";
- TypeScript 在变量声明的时,使用
:
对变量进行类型的声明 - TypeScript 遵循强类型,如果将不同的类型赋值给变量会编译错误
- 一个变量可以使用
|
来支持多种类型,称为联合类型
let x: number | null | undefined;
x = 1; // 运行正确
x = undefined; // 运行正确
x = null; // 运行正确
但是,TypeScript 除了 js 中已有类型外,还有其他两种类型,any
和 never
- any
- 表示变量可以为任何类型,针对类型不明确的变量。
- 变量如果在声明的时候,未指定其类型,也未被赋值,那么它会被识别为 any 类型
- never
- 表示其它类型(包括 null 和 undefined)的子类型,代表从不会出现的值。
- 声明为 never 类型的变量只能被 never 类型所赋值
- 声明为其他类型的变量可以被 never 类型所覆盖,但是无法再改回原类型
# 推断
当类型没有给出时,TypeScript 编译器利用类型推断来推断类型。
var num = 2; // 类型推断为 number
console.log("num 变量的值为 " + num);
num = "12"; // 编译错误
console.log(num);
# 别名
类型别名,通过 type
关键字来给一个类型起个新名字,常用于联合类型。
type str = string;
type num = number;
type mixin = str | num;
var test: mixin = "mixin type";
console.log(test);
# 断言
在 TypeScript 可以使用类型断言来手动指定一个值的类型,即允许变量从一种类型更改为另一种类型。
为了在进行类型断言时提供额外的安全性,可以使用 any
类型断言有两种写法
一种是使用尖括号的方式
let str = "this is a string";
let str2: number = (<string>(<any>str)).length;
console.log(str2);
一种是使用 as 语法,在 JSX 或 TSX 中,推荐使用这种方式
var str: any = "this is a string";
var str2: number = (str as string).length;
console.log(str2);
在还不确定类型的时候就访问其中一个类型的属性或方法,就使用类型断言
个人认为,类型断言太过于鸡肋,使用类型保护会更合适些,详情可以点击这里
类型断言不是类型转换,而是指定为一个更加具体的类型
# 函数声明
在函数中,可以对函数的形参进行类型声明,还可以指定函数返回的类型
// function getLen(len: string | number): number {
// if((len as string).length){
// return (len as string).length
// }else{
// return len.toString().length
// }
// }
function getLen(len: string | number): number {
if (typeof len === "string") {
return len.length;
} else if (typeof len === "number") {
return len.toString().length;
}
}
var len = getLen(666);
console.log(typeof len);
console.log(len + 1);
在上面的例子中,getLen
有两种方式能够达到一样的效果:一种是使用类型断言,一种是使用 typeof
类型保护。倾向于第二种。
函数返回值的声明可以为 void
,表示没有返回值。另外, TypeScript 中有函数重载的功能,条件和其他高级语言差不多,不再多述
对上述例子可修改为
function getLen(len: number): number;
function getLen(len: string): void;
function getLen(len: number | string): number | void {
if (typeof len === "string") {
console.log(len);
} else if (typeof len === "number") {
return len.toString().length;
}
}
var len = getLen(666);
console.log(typeof len);
console.log(len + 1);
getLen("666");
# 数组声明
与变量声明一样,如果没有不遵循定义好的类型,依旧会报错。
在 TypeScript 中,数组可以使用 「类型 + 方括号」表示法
var arr: string[] = ["test", "arr", "declare"];
var array: any[] = ["test", "array", "declare", 2];
var multi: number[][] = [
[1, 2, 3],
[23, 24, 25]
];
console.log(arr);
console.log(array);
console.log(multi[0][0]);
TypeScript 中有个元组的概念,在我看来,其实就是未声明类型的数组,当作写 es6 即可
# 枚举
默认情况下,枚举是基于 0 的,也就是说第一个值是 0,后面的值依次递增。
这是枚举类型基本的例子
enum ACGN {
Animation,
Comic,
Game,
Novel
}
let A = ACGN.Animation;
console.log(A); // 0
console.log(ACGN[0]); // Animation
可以看到编译后的文件其实是先用闭包的方式初始化了一个对象
var ACGN;
(function(ACGN) {
ACGN[(ACGN["Animation"] = 0)] = "Animation";
ACGN[(ACGN["Comic"] = 1)] = "Comic";
ACGN[(ACGN["Game"] = 2)] = "Game";
ACGN[(ACGN["Novel"] = 3)] = "Novel";
})(ACGN || (ACGN = {}));
var A = ACGN.Animation;
console.log(A);
console.log(ACGN[0]);
一般使用枚举类型,是为了可以定义一些有名字的数字常量,方便进行维护。
假设后台对 ACGN 是使用 0123
来进行存储的,而不是使用名字,这时我们就可以采用枚举类型,再向后台传数据的时候就可以直接写 ACGN.Animation
这样就可以不用专门声明一个对象进行数据存储,更加语义化
另外除了数字枚举,TypeScript 可以支持字符串枚举
enum ACGN {
Animation = "Animation",
Comic = "Comic",
Game = "Game",
Novel = "Novel"
}
let A = ACGN.Animation;
console.log(A); // Animation
console.log(ACGN[0]); // 这里会报错
另外可以对枚举类型进行计算
enum ACGN {
Animation = 1,
Comic,
Game,
Novel
}
let A = ACGN.Animation;
console.log(A); // 1
console.log(ACGN[1]); // Animation
但是并不推荐使用这种计算
大多数情况下,枚举是十分有效的方案。 然而在某些情况下需求很严格。 为了避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问,我们可以使用常量枚举。 常量枚举通过在枚举上使用 const
饰符来定义。
# 泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。
简单来看下使用方式
function get<T>(param: T): T {
return param;
}
let num = get<number>(1);
console.log(typeof num); // number
console.log(num); // 1
console.log("------------");
let str = get<string>("1");
console.log(typeof str); // string
console.log(str); // 1
console.log("------------");
let bool = get<boolean>(true);
console.log(typeof bool); // boolean
console.log(bool); // true
console.log("------------");
let arr = get<string[]>(["test", "arr", "generic"]);
console.log(typeof arr); // object
console.log(arr); // [ 'test', 'arr', 'generic' ]
简单来说,就是以 T 为模板,通过传参,确保函数的形参和返回值是一致的,达到可重用的目的。
接下来更复杂一点,创建泛型接口,可以更直观地发现函数使用的类型
interface judgeType<T> {
(param: T): T;
}
function get<T>(param: T): T {
return param;
}
let NumGet: judgeType<number> = get;
let StrGet: judgeType<string> = get;
let num = NumGet(1);
console.log(typeof num);
console.log(num);
console.log("------------");
let str = StrGet("1");
console.log(typeof str);
console.log(str);
既然能创建泛型接口,那就可以创建泛型类
class Generic<T> {
sum: (x: T, y: T) => T;
}
let numClass = new Generic<number>();
let strClass = new Generic<string>();
numClass.sum = (x, y) => {
return x + y;
};
strClass.sum = (x, y) => {
return x + y;
};
let num = numClass.sum(5, 20);
console.log(typeof num);
console.log(num);
console.log("------------");
let str = strClass.sum("5", "20");
console.log(typeof str);
console.log(str);
泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。
定义泛型的时候,可以一次定义多个类型参数:
function swap<T, U>(arr: [T, U]): [U, T] {
return [arr[1], arr[0]];
}
swap([7, "seven"]); // ['seven', 7]
# 接口
接口是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,需要由具体的类去实现,然后第三方就可以通过这组抽象方法调用,让具体的类执行具体的方法。
# 声明
在 TypeScript 中,常用 interface 来自定义类型规则,可以与联合类型一起使用。一般用于类和对象中,也可以用于数组和函数
interface Person {
readonly id: number | string; // 设置只读,不可更改
name: string;
age: number;
sex?: string; // 可选
talk: (word: string) => void;
[propName: string]: any; // 表示可以添加任意属性
}
let Author: Person = {
id: "1",
name: "hzk",
age: 24,
talk: (word: string) => {
console.log(word);
},
like: "music",
hate: "lazy"
};
console.log(Author);
Author.talk("Author is hzk");
//Author.id = 2 // 编译错误
Author.age = 18;
console.log(Author);
# 继承
接口继承就是说接口可以通过其他接口来扩展自己。
TypeScript 允许接口继承多个接口,使用逗号隔开
interface Hobby {
like: string;
hate: string;
}
interface Person {
readonly id: number;
name: string;
age: number;
sex?: string;
talk: (word: string) => void;
}
interface Programmer extends Person, Hobby {
position: string;
level: number | string;
}
let Author: Programmer = {
id: 1,
name: "hzk",
age: 24,
talk: (word: string) => {
console.log(word);
},
position: "FEG",
level: "primary",
like: "music",
hate: "lazy"
};
console.log(Author);
Author.talk("Author is hzk");
Author.age = 18;
console.log(Author);
# 类
# 定义
定义类的关键字为 class,后面紧跟类名。在类中一般需要定义三个模块
- 字段属性
- 构造函数
- 方法
class Person {
name: string;
age?: number;
sex: string;
constructor(name: string, sex: string, age?: number) {
this.name = name;
this.sex = sex;
this.age = age;
}
say(): void {
console.log(`name:${this.name}`);
console.log(`sex:${this.sex}`);
if (this.age) {
console.log(`age:${this.age}`);
} else {
console.log("age secret");
}
}
}
let male = new Person("Mike", "male", 20);
let fmale = new Person("Sophie", "fmale");
male.say();
console.log("----------------");
fmale.say();
类的定义好像很简单,emmm,那么将上面的例子再改的复杂一点吧
class Person {
public static num = 0; // 静态属性
private id: number;
public name: string; // 一般默认都是 public
private age?: number; // 只能在类本身访问
protected sex: string; // 只能在类本身或者子类中进行访问,实例化的对象无法访问
public constructor(name: string, sex: string, age?: number) {
this.name = name;
this.sex = sex;
this.age = age;
this.id = Person.init(); // 使用类名来调用静态方法
}
public static init(): number {
this.num += 1;
return this.num;
}
public say(): void {
console.log(`id:${this.id}`);
console.log(`name:${this.name}`);
console.log(`sex:${this.sex}`);
if (this.age) {
console.log(`age:${this.age}`);
} else {
console.log("age secret");
}
}
}
let male: Person = new Person("Mike", "male", 20);
var fmale: Person = new Person("Sophie", "fmale");
male.say();
console.log("----------------");
fmale.say();
// console.log(male.age) // 私有属性报错
在新的例子中,主要添加了以下三个细节
- 使用访问修饰符,确保类的安全
- 使用
static
关键字来定义静态属性和静态方法 - 给类加上 TypeScript 的类型
# 与接口一起使用
在使用接口之前,需要知道,类和接口一样都是可以继承的。结合后的例子如下:
interface Hobby {
like: string;
hate: string;
talk: (like: string, hate: string) => void;
}
class Programmer {
position: string;
level: number | string;
public setProgrammer(position: string, level: number | string): void {
this.position = position;
this.level = level;
}
}
class Person extends Programmer implements Hobby {
public static num = 0;
private id: number;
public name: string;
private age?: number;
protected sex: string;
public like: string;
public hate: string;
public constructor(name: string, sex: string, age?: number) {
super();
this.name = name;
this.sex = sex;
this.age = age;
this.id = Person.init();
}
public static init(): number {
this.num += 1;
return this.num;
}
public say(): void {
console.log(`id:${this.id}`);
console.log(`name:${this.name}`);
console.log(`sex:${this.sex}`);
if (this.age) {
console.log(`age:${this.age}`);
} else {
console.log("age secret");
}
}
public setProgrammer(position: string, level: number | string): void {
super.setProgrammer(position, level);
}
}
let male: Person = new Person("Mike", "male", 20);
var fmale: Person = new Person("Sophie", "fmale");
male.setProgrammer("FEG", "middle");
male.say();
male.talk("life", "job");
console.log("----------------");
fmale.say();
使用方法和 Java
,C#
等语法类似。TypeScript 中类可以继承多个,接口也可以继承多个
在继承的时候,构造函数需要使用 super()
运行父类的构造函数,在自定义的方法中,也可以使用 super
关键字来引用父类的方法
# 抽象类
abstract class Animal {
public static num = 0;
public name: string;
protected age?: number;
protected sex: string;
public constructor(name: string, sex: string, age?: number) {
this.name = name;
this.sex = sex;
this.age = age;
}
public abstract say();
public static init(): number {
this.num += 1;
return this.num;
}
}
class Person extends Animal {
private id: number;
public name: string;
protected age?: number;
protected sex: string;
public constructor(name: string, sex: string, age?: number) {
super(name, sex, age);
this.id = Person.init();
}
public say(): void {
console.log(`id:${this.id}`);
console.log(`name:${this.name}`);
console.log(`sex:${this.sex}`);
if (this.age) {
console.log(`age:${this.age}`);
} else {
console.log("age secret");
}
}
}
let male: Person = new Person("Mike", "male", 20);
var fmale: Person = new Person("Sophie", "fmale");
male.say();
console.log("----------------");
fmale.say();
抽象类和接口的区别
- 抽象类是用来捕捉子类的通用特性的 。它不能被实例化,只能被用作子类的超类。抽象类是被用来创建继承层级里子类的模板。如果想要有一些方法可以默认实现的话,那就应该使用抽象类
- 接口是抽象方法的集合。如果一个类实现了某个接口,那么它就继承了这个接口的抽象方法。这就像契约模式,如果实现了这个接口,那么就必须确保使用这些方法。接口只是一种形式,接口自身不能做任何事情。 跟其他语言不同的是,JavaScript 的接口不仅仅知识用来存放方法,也可以存放字段。
关于类有好多知识点,最好是有后端基础更容易来理解。
# 模块化
# 命名空间
先看下基本的例子
namespace Api {
export interface requestData {
id: number;
}
export class Auth implements requestData {
public id: number;
public getUserInfo(id: number): Object {
this.id = id;
return {
name: "hzk",
position: "FE"
};
}
public deleteUserInfo(id: number): boolean {
this.id = id;
return false;
}
}
}
interface requestData {
id: number;
}
class Auth implements requestData {
public id: number;
public getUserInfo(id: number): Object {
this.id = id;
return {
name: "hzk",
position: "whole stack"
};
}
public deleteUserInfo(id: number): boolean {
this.id = id;
return true;
}
}
var authApi = new Api.Auth();
var globalAuth = new Auth();
let dataA = globalAuth.getUserInfo(1);
let boolA = globalAuth.deleteUserInfo(1);
let dataB = authApi.getUserInfo(1);
let boolB = authApi.deleteUserInfo(1);
console.log(dataA);
boolA ? console.log("删除成功") : console.log("删除失败");
console.log("-------------------");
console.log(dataB);
boolB ? console.log("删除成功") : console.log("删除失败");
{ name: 'hzk', position: 'whole stack' }
删除成功
-------------------
{ name: 'hzk', position: 'FE' }
删除失败
例子中,定义了一个 Api
的命名空间,使用命名空间后 可以解决重名问题 相当于设置了一个 scoped
事实上,阅读编译后的文件后可以发现,确实是设置了闭包才确定作用域。另外,命名空间可以进行嵌套使用
在早期的 TypeScript 中,命名空间有另外一个称呼为“内部模块”,接下来要学习的“模块”,则是被称为“外部模块”
# 模块
TypeScript 模块的设计理念是可以更换的组织代码。
对于现在的前端开发者来说,模块化已经是一个老生常谈的概念了。如果不了解的可以去看下 Node.js 和 ES6
先看下具体例子
api.ts
interface requestData {
id: number;
}
export class Auth implements requestData {
public id: number;
public getUserInfo(id: number): Object {
this.id = id;
return {
name: "hzk",
position: "FE"
};
}
public deleteUserInfo(id: number): boolean {
this.id = id;
return false;
}
}
export class Test implements requestData {
public id: number;
public done(): void {
console.log("Test");
}
}
main.ts
import * as Api from "./api";
import { Test } from "./api";
var authApi = new Api.Auth();
let data = authApi.getUserInfo(1);
let bool = authApi.deleteUserInfo(1);
console.log(data);
bool ? console.log("删除成功") : console.log("删除失败");
console.log("-------------------");
var test = new Test();
test.done();
在上面的例子中可以发现,TypeScript 也一样,使用 import
来导入模块,使用 export
来导出模块
tsc main.ts
后,会发现编译出了两个文件, main.js
和 api.js
编译后的 js 文件默认是使用 CommonJS 规范的,如有需要,可以使用 tsc --module amd main.ts
来转成 AMD 规范,更多可以点击 这里
不应该对模块使用命名空间,使用命名空间是为了提供逻辑分组和避免命名冲突。 模块文件本身已经是一个逻辑分组,并且它的名字是由导入这个模块的代码指定,所以没有必要为导出的对象增加额外的模块层。
# 声明文件
在 TypeScript 中,我们使用 “模块” 和 “命名空间” 来更好地编写我们的应用,但是当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
TypeScript 中是使用 .d.ts
的文件来表示声明文件。书写声明文件不是一件简单的事情,具体可以点击 这里 和 这里
关于 TypeScript 的模块化需要先了解 三斜线指令 ,且仅当在你需要写一个 .d.ts
文件时才使用这个指令。这里只是简单地写下例子,更多地需要慢慢挖掘。
api.js
var Api;
(function(Api) {
var Auth = /** @class */ (function() {
function Auth() {}
Auth.prototype.getUserInfo = function(id) {
return {
name: "hzk",
position: "FE"
};
};
Auth.prototype.deleteUserInfo = function(id) {
return false;
};
return Auth;
})();
Api.Auth = Auth;
function log(content) {
console.log(content);
}
Api.log = log;
var ACGN;
(function(ACGN) {
ACGN[(ACGN["Animation"] = 0)] = "Animation";
ACGN[(ACGN["Comic"] = 1)] = "Comic";
ACGN[(ACGN["Game"] = 2)] = "Game";
ACGN[(ACGN["Novel"] = 3)] = "Novel";
})((ACGN = Api.ACGN || (Api.ACGN = {})));
})(Api || (Api = {}));
module.d.ts
declare module Api {
export class Auth {
public getUserInfo(id: number): Object;
public deleteUserInfo(id: number): boolean;
}
export function log(content: any): void;
export enum ACGN {}
}
main.ts
/// <reference path="module.d.ts"/>
var auth = new Api.Auth();
Api.log(auth.getUserInfo(1));
Api.log(Api.ACGN);
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<script src="api.js"></script>
<script src="main.js"></script>
</body>
</html>
在示例中,是先自己简单制作一个第三方库 api.js
,然后声明文件后,在 main.ts
中进行引用。
最后将文件引入到 index.html
中,打开浏览器后就可以看到对应的结果
"声明文件"的意思和它的术语一样,只能进行声明,不能写具体的实现方法。
# 其他
在学习 Typescript 的时候,发现还可以使用一个 @Reflect.metadata
的装饰器来处理原数据。个人认为,可以简单理解为直接挂在类上的一个 static 属性, 只是使用 Reflect
相关的方法来处理数据
# 总结
从功能上来说,TypeScript 更主要的其实是对类型的约束吧,对于开发可能帮助比较小,但是对于维护来说,因为有很健全的类型系统和规范,能够快速找到对应的错误,减少一些未知的 bug
在学习 TypeScript 的过程中,我更加深刻地了解到,它并不是一种新语言,而是一种封装,正如它自己的介绍一般,它是 JavaScript 的超集!
这篇文章并没有将 TypeScript 全部学完,只是简单地学习 80% 的基础内容。之后会继续学习对应的 Symbol,Decorator,TSX,Mixin 等内容