什么是TypeScript?#
TypeScript是微软公司2009年发布的一个开源的JavaScript超集语言,它的设计者是C#语言之父安德斯·海尔斯伯格。
JavaScript超集:当前任何JavaScript都是合法的TypeScript代码。
TypeScript主要为JavaScript提供了类型系统和ES6+语法支持。
与Flow相比,Flow是一个类型检查工具,TypeScript是一种开发语言。
TypeSCript有自己的编译器,可以将写好的TypeScript代码最终通过编译器编译成JavaScript代码进行运行。
为什么要给javascript加上类型?#
- 类型系统可以提高代码质量和可维护性。
- 有利于代码重构,在编译时捕获错误,提供类型安全,提高代码健壮性。
- 类型系统是一个出色的文档形式,方便大型项目的协同开发,降低团队成员沟通成本。
安装和配置TypeScript#
TypeScript最终要运行起来,需要将TypeScript转换成JavaScript代码。转换可以通过TypeScript的命令行工具来完成。
安装:
npm install -g typescript
执行以上命令会在全局环境下安装tsc命令。
编写typescript代码:
示例代码:
新建一个hello.ts的文件
let num: number = 100
num = 1234
function sum(a: number, b: number): number {
return a + b;
}
const total: number = sum(10, 20)
编译:
tsc hello.ts
通过上面这个命令可以将hello.ts文件编译成js文件。tsc只负责编译将ts编译为js,而没有直接运行ts的功能。要想直接运行ts,可以全局安装 npm i -g ts-node
,终端输入 ts-node ./hello.ts
即可直接查看运行结果。
初始化:
tsc --init
使用以上命令可以在当前目录下生成一个tsconfig.json的文件,用来配置当前项目的相关内容。
tsconfig.json
{
"compilerOptions": {
/* 基本选项 */
"target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
"module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
"lib": [], // 指定要包含在编译中的库文件
"allowJs": true, // 允许编译 javascript 文件
"checkJs": true, // 报告 javascript 文件中的错误
"jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
"declaration": true, // 生成相应的 '.d.ts' 文件
"sourceMap": true, // 生成相应的 '.map' 文件
"outFile": "./", // 将输出文件合并为一个文件
"outDir": "./", // 指定输出目录
"rootDir": "./", // 用来控制输出目录结构 --outDir.
"removeComments": true, // 删除编译后的所有的注释
"noEmit": true, // 不生成输出文件
"importHelpers": true, // 从 tslib 导入辅助工具函数
"isolatedModules": true, // 将每个文件作为单独的模块 (与 'ts.transpileModule' 类似).
/* 严格的类型检查选项 */
"strict": true, // 启用所有严格类型检查选项
"noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
"strictNullChecks": true, // 启用严格的 null 检查
"noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误
"alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'
/* 额外的检查 */
"noUnusedLocals": true, // 有未使用的变量时,抛出错误
"noUnusedParameters": true, // 有未使用的参数时,抛出错误
"noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
"noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)
/* 模块解析选项 */
"moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
"baseUrl": "./", // 用于解析非相对模块名称的基目录
"paths": {}, // 模块名到基于 baseUrl 的路径映射的列表
"rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
"typeRoots": [], // 包含类型声明的文件列表
"types": [], // 需要包含的类型声明文件名列表
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。
/* Source Map Options */
"sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
"mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
"inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
"inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性
/* 其他选项 */
"experimentalDecorators": true, // 启用装饰器
"emitDecoratorMetadata": true // 为装饰器提供元数据的支持
}
}
配置项:
- "outDir": "./dist" : 生成文件的存放目录
- "rootDir": "./src" :ts代码的存放路径
- "strict": true: 将TypeScript代码转换成严格模式的代码
- "target": "es5": 将ts代码转换成js代码的版本(默认是es5)
- "module": "commonjs":将ts代码转换后,使用的模块化标准。(默认commonjs)
声明空间#
-
类型声明空间:
用来存储数据类型注解,仅用于对变量做类型注解,不能直接当作变量使用。
class Foo {} interface Car {} type Phone {} const a: Foo; const b: Car; const c: Phone;
-
变量声明空间:
用来存储具体数据,可以直接使用。
const a: number = 1111; const b: string = "hello word";
命名空间:#
用于区分某些边界问题创建独立作用域,在js中可以通过匿名函数或空对象来实现。ts则提供了namespace 关键字用来表述某个作用域块。
// 使用匿名函数创建独立作用域
(function(a,b,c){
// some code
})(a,b,c)
// 使用空对象创建独立作用域
{
function test() {
// some code
}
const abc: number = 111;
}
// 使用ts创建独立作用域
namespace Test {
// some code
}
模块#
模块的概念:当一个项目存在多个文件,多个文件相互关联,且每个文件有自己的作用域时,每个文件就是一个模块,在模块内部定义的变量、函数、类都是私有的,对其他文件不可见。只能通过模块化规范中的特定语法来暴露给其他文件使用,其他文件可以使用模块化规范中的语法来导入这些模块暴露的数据进行使用。
模块化规范#
目前模块化标准较多只介绍两种常用用到的。
-
CommonJS: Node.js所使用的模块化系统就是基于CommonJS规范实现的,它的模块加载过程是同步的,支持动态导入。可以在if等语句中执行加载。
// 111.js const name = '1' const age = '2' module.exports.name = name; // 导出一个变量 module.exports.age = age // 导出另一个变量 module.exports = age; // 默认导出一个变量 (一个模块内默认导出与其他导出语句不能同时存在,且一个文件只能有一个默认导出) // 222.js const obj = require('./111.js') // { name: '1', age: '2'} const age = require('./111.js') // '2'
-
ESM: ES6 Module是JavaScript的模块化规范,使用ES6模块化会自动启用严格模式,即使没在文件顶部添加'use strict' 语句。ESM加载模是同步还是异步取决于项目中的使用,一般项目中使用的是静态同步的导入,导入只能在当前模块的最顶层作用域中。在webpack中也可以通过使用插件(
@babel/plugin-syntax-dynamic-import
)来支持异步动态(运行时)导入(分包懒加载就是通过这种方法实现的)。ESM异步导入目前处于TC39提案阶段。// 111.ts const num1: number = 1111; const str1: string = 'aaa'; const obj1: object = {}; export num1; // 导出某一项 export num2; // 导出某一项 export default obj1; // 默认导出 // 222.ts import obj1, { num1, str1 } from './111.ts'; // 导入方式 import * as someValue from './111.ts'; // 导入全部模块并赋值给一个变量 import './111.ts'; // 只执行导入
模块路径#
-
相对路径:根据当前文件的所在位置导入。(webpack在配置中有个extensions字段可以设置默认后缀名,导入时可以省略后缀不写)例如
../../xxx/index
-
动态路径:当导入语句不以
./
或../
开头时,导入操作会动态查找当前目录下的node_modules/xxx
模块,如果不存在则逐级从项目文件夹向上查找node_modules文件夹,一种到根目录和全局node_modules。 -
重写类型动态查找:在项目中可以可以通过声明全局模块的方式来解决查找模块路径的问题。
// 111.ts declare module 'abc' { export const a = '1' export const b = '2' } // 222.ts import * as soneModule from 'abc';
定义模块#
与ts的全局变量类似,可以通过 declare module 来定义一个模块,这个模块可以像安装的npm包一样使用。对于绝大多数比较流行的库,社区已经提供了@types 的类型声明,如果没有这个类型声明,就可以通过这种方式来实现。
declare module 'jquery' { // 这里用来定义为模块
export const query = xxxx
}
import * from 'jquery' // 这里可以不写相对路径,直接引入使用
数据类型#
数值类型(Number)#
const a: number = 123 // 数值
const b: number = NaN // 非数字
const c: number = Infinity // 正无穷大的数值
const d: number = 0xA12 // 十六进制
const e: number = 0b1010101 // 二进制
const f: number = 0o75 // 八进制
字符串类型(String)#
const str1: string = "hello"
const str2: string = 'hello'
const str3: string = `hello`
布尔类型 (Boolean)#
const flag1: boolean = true
const flag2: boolean = false
数组(Array)#
const arr1: Array<number> = [1, 2, 3]
const arr2: number[] = [1, 2, 3]
元祖(Tuple)#
元祖类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。
const arr1: [number, string] = [100, 'hello']
空值(void)#
某种程度上来说,void类型像是与any类型相反,它表示没有任何类型,当一个函数没有返回值时,通常会见到返回值类型是void。
声明一个void类型没什么大用。因为它只能赋值为undefined
function foo():void {
alert('hello word')
}
const result: void = undefined
undefined#
undefined 类型只能赋值为undefined
null#
null类型只能赋值为null
any#
any类型可以赋值为任意类型
let val: any = 100
val = 'hello'
val = {}
val = []
Never#
never类型表示的是那些用不存在的值的类型。例如, never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。变量也可能是naver类型,当他们被用部位真的类型保护所约束时。
never类型时任何类型的子类型,也可以赋值给任何类型;然而,没有类型时never的子类型或可以赋值给nerver类型(除了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)#
const obj1: { name: string, age: number } = { name: 'xiaoming', age: 20 }
枚举(Enum)#
enum类型是对JavaScript标准数据类型的一个补充。像其他语言一样,使用枚举类型可以为一组数值赋予友好的名字。
enum Color {
red,
green,
blue,
}
const green: Color = Color.green;
默认情况下从0开始为元素编号,数字枚举带有自增长,如果不指定具体值,则默认从0开始自增。也可以手动的制定成员的数值。例如,我们将上面的例子改成从1开始编号。
enum Color2 {
red = 1,
green,
blue
}
const red: Color2 = Color2.red
或者,全部都采用手动赋值
enum Color3 {
red = 1,
green = 2,
blue = 3
}
const blue: Color3 = Color3.blue
字符串枚举
在typescript版本支持了字符串枚举,每个成员都必须使用字符串字面量。
Enum Colors {
red = 'RED',
green = 'GREEN',
blue = 'BLUE'
}
类型断言#
有时你会遇到这样的情况,你会比typescript更了解某个值的详细信息,通常这回发生在你清楚地知道一个实例具有比他现有类型更确切的类型。
通过类型断言这种方式可以告诉编译器,类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在表意阶段起作用,typescript会假设你,已经进行了必须的检查。
类型断言有两种形式:
其一是尖括号语法:
const someValue: any = 'this is a string'
const strLength: number = (<string>someValue).length
另一个为as语法:在react项目中推荐这种写法防止与jsx产生冲突
const someValue: any = 'this is a string'
const strLength: number = (someValue as string).length;
两种形式的等价的,可以凭个人喜好使用。然而,在typescript里使用jsx时,只有as 语法断言是被允许的,这也是为了防止和react中的jsx元素产生冲突。
联合类型#
当一个字面量有可能为多种类型时,联合类型可以把多个类型结合起来用于约束某一个字面量,字面量只能为多种类型中的其中一种。多个类型之间用 | 分割。
const type1: string;
// 联合类型
const type2: number | boolean;
const type3: string | object;
交叉类型组合类型#
在需要把多个类型合并成一个类型进行处理时,交叉类型在这个时候非常有用。它是有多个类型组合而成的,具有各个类型的所有属性。这与上面提到的联合类型刚好相反,它可以将多个类型组成一个类型。这里有个问题时如果两个个类型中有相同的属性,这个相同属性的类型相同则不会报错,如果属性相同类型不同,组合起来就会报错。解决办法是把相同属性的类型改为联合类型,但从理解上来看他们应该有不同的行为,不应该强制合并到一起,更好的做法是使用不同的名称来区分。交叉类型对基本类型、接口、类、泛型等都适用。
class A {
width: number = 0;
height: number = 0;
}
class B {
name: string = '',
type: string = ''
}
function make(size: A, goods: B): A & B {
const Book = <A & B> {}
Book.width = 1
Book.height = 1
Book.name = 'abc'
Book.type = 'book'
return Book
}
类(class)#
介绍
使用函数和基于原型的继承来创建可重用的代码。但对于熟悉使用面向对象方式的开发者来讲就有些棘手。因为他们用的是机遇类型的继承斌切对象是由类构建出来的。从ECMAScript2015,也就是ES6开始,JavaScript程序员将能够使用基于类的面相对象的方式。使用typescript,我们允许开发着现在就使用这些特性。并且编译后的JavaScript可以子所有主流浏览器和平台上运行,而不需要等到下个JavaScript版本。
一个简单的例子:
class Greeter {
// 与ES6不同的是,TS中属性必须声明,需要指定类型
greeting: string
// 声明好属性之后,属性必须赋值一个默认值后者在构造函数中进行初始化。
constructor(message: string) {
this.greeting = message;
}
greet() {
return 'htllo' + this.greeting
}
}
let greeter = new Greeter('word')
类的继承#
class Say {
name: string
constructor(name: string) {
this.name = name
}
say() {
console.log(this.name + " say good morning")
}
}
class Person extends Say {
}
const john = new Person('john')
john.say()
class Say {
name: string
constructor(name: string) {
this.name = name
}
say() {
console.log(this.name + " say good morning")
}
}
class Person extends Say {
type: string
constructor(type: string, name: string) {
super(name)
this.type = type
}
// 子类中如果出现了和父类同名的方法,则会进行合并覆盖
age() {
console.log(this.name + ' age is ' + this.type)
}
}
const john = new Person('jerry', '20')
john.say()
john.job()
访问修饰符#
访问修饰符指的基石可以在类的成员前通过添加关键字来设置当前成员的访问权限
- public: 公开的,默认,所有人都可以进行访问。
- private: 私有的,只能在当前类中进行访问。
- protected: 受保护的,只能在当前类或者子类中进行访问。
enum CarColor {
red = 'red',
green = 'green',
blue = 'blue'
}
class Car {
// 如果不加访问修饰符,则默认是公开的,所有人都可以访问
color: string
constructor() {
this.color = CarColor.red
}
protected go() {
console.log('go to school')
}
}
const BYD = new Car()
BYD.color
class Audi extends Car {
// 添加了private,只能在当前的类中使用
private run() {
this.go() // protected 修饰后在子类可以正常访问
console.log(this.color)
}
}
const audi = new Audi()
audi.color
// 报错; run方法使用了private修饰外面访问不到
// audi.run()
readonly修饰符
使用readonly管检测可以将属性设置为只读的,只读属性必须在声明时或者构造函数里被初始化。
class Person {
readonly name: string;
readonly numberOfStep: number = 8
constructor(theName: string) {
this.name = theName
}
}
const QaQ = new Person('fdsffsdfsf')
QaQ.name = 'fsdfsdfsdfsd' // 报错!name是只读的,不能赋值
参数属性
在上面例子中,我们不得不定义一个受保护的成员name和一个构造函数参数theName在Person类里,并且立即将theName的值赋值给name。这种情况经常会遇到。参数属性可以方便地让我们在一个地方定义并初始化一个成员。下面例子是对之前Animal类的修改版,使用了参数属性。
class Person2 {
constructor(private name: string) { }
move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}`)
}
}
存取器#
class People {
private _name: string = ''
// 当访问name属性的时候会自动调用这个方法,返回值为访问此属性获取到的值
get name(): string {
return this._name
}
// 当修改name属性的时候会自动调用这个方法,返回值为修改此属性的值
set name(value: string) {
// 设置器中可以添加相关的校验逻辑
if (value.length < 2 || value.length > 5) {
throw new Error('名称不合法')
}
this._name = value
}
}
let p = new People()
p.name = 'hello word'
接口(interface)#
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 {
readonly label: string // 在接口中也可以设置只读属性
name?: string // 加上?代表可选属性,可以传也可以不传
success(msg: string): void
[propName: string]: any // 额外的属性检查,如果多传了额外的参数自动忽略掉
}
function printLabel(options: LabelledValue) {
}
printLabel({ label: 'hello word', success() { } })
函数类型接口#
interface SumInterface {
(a: number, b: number): number
}
const sum: SumInterface = function (a: number, b: number): number {
return a + b
}
类类型的接口#
interface PersonInterface {
name: string
age: number
say(): void
}
class Jack implements PersonInterface {
name: string = 'jack'
age: number = 18
say() { }
}
接口继承接口#
interface OneInterface {
x: number,
y: number,
}
interface TwoInterface extends OneInterface {
z: number
}
const Print: TwoInterface = {
x: 10,
y: 20,
z: 30,
}
多继承接口(一次性继承多个接口)
interface OneInterface {
x: number,
y: number,
}
interface TwoInterface extends OneInterface {
z: number
}
interface ThreeInterface extends OneInterface, TwoInterface {
date: Date
}
const Print2: ThreeInterface = {
x: 10,
y: 20,
z: 30,
date: new Date()
}
接口继承类#
class Boy {
name: string = ''
say(): void { }
}
interface Say extends Boy {
}
const Even: Boy = {
name: 'Even',
say() { }
}
Even.say()
泛型(Generies)#
泛型的概念:
如果一个函数它的入参是any类型的,返回值也是any类型。如果输入和输出可以是任意类型,就会造成类型丢失,使得后续代码无法继续推导类型。而泛型就是解决这个问题的,它可以让比较灵活的参数,保留数据类型,让类型ts编译器的推导变得更准确不会丢失某些信息。
简单来讲泛型是一种对未知类型的定义。相当于一个用来存储类型注解的零时占位符,当一个函数传入参数时这个零时占位符就保存下了这个传入参数的类型,使它的返回结果也能够得到更准确的推断。
function echo(arg: any) { // 返回结果不确定类型
return arg
}
// 根据入参,返回某个具体的确定的类型
function echo<T>(arg: T): T {
return arg
}
// 泛型函数
function foo<T, P, N>(a: T, b: P, c: N): [N, P, T]{
return [c, b, a]
}
// 泛型类
class add <T> {
count: T;
add: (x: T, y: T) => T;
}
// 泛型约束
// 当入参不确定,但又需要保证入参必须具有某个属性时,可以通过结合 interface + extends来对入参做约束,防止代码在运行过程中抛出异常。
interface hasLength {
length: number;
}
function foo <T extends hasLength>(arg: T): T {
console.log(arg.length)
return arg
}
装饰器(Decorator)#
装饰器虽然在ECMA中处于提案阶段,但是通过babel等构建工具可以转译,在项目上早以普遍适用。typescript把它作为实验性功能进行了引入。这种语法与APO技术比较相似,AOP技术可以切开一段代码并将其分离到其他位置,从而提取重复性代码,减少样板性代码。开启装饰器功能可以在tsconfig.json中设置 experimentDecorators: true
interface ITips { // 接口定义两种提示信息
Log(text: string): void;
Warn(text: string): void;
}
class Print implements ITips { // 用于在控制台打印内容
Log(text: string): void {
console.log(text)
}
Warn(text: string): void {
console.warn(text)
}
}
const info1 = { type: 'log', text: 'this is text' };
const info2 = { type: 'warn', text: 'this is warn' };
function foo (someMthod: ITips) {
console.log(`current type: ${info1.type}`)
someMthod.Log(info1)
someMthod.Warn(info1)
}
foo(new Print())
以上面代码为例,预期结果是根据不同类型执行不同的打印方法,把结果打印输出到控制台,
Log方法只在type为log时调用,Warn方法只在type为warn时调用,而不是两个方法都执行。要修改这段代码就要在每个方法上面加一些逻辑 嵌套一层 if 语句判断用以区分,如果类型较多都要判断,那么修改量大,容易引入不易察觉的bug。这里用装饰器就可以转移样板代码,同时也解决了单一职责问题,把判断的逻辑抽离开。
装饰器接收3个参数:
- 参数一 要装饰的对象
- 参数二 所要装饰的属性名
- 参数三 该属性的描述对象,用来修改某些方法的行为
function fn(target, name, descriptor) {
// descriptor是一个对象 它的值如下:
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// }
}
通过装饰器修改原方法:
function exec(type: string) {
return function(target: any, name: string, descriptor: any) {
}
}
搭建ts项目#
基本的ts项目示例:#
这里以react + ts为例创建一个简单的ts-demo项目。
-
创建目录并初始化:
mkdir ts-demo && cd ts-demo && npm init -y && touch webpack.config.js && mkdir src && touch ./src/index.tsx && npm i typescript webpack webpack-cli webpack-dev-server react react-dom babel-loader babel-preset-react-app html-webpack-plugin @types/react
-
webpack.config.js配置文件:
// webpack.config.js
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
process.env.NODE_ENV = 'development'
module.exports = {
entry: './src/index.tsx',
output: {
path: resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js']
},
plugins: [
new HtmlWebpackPlugin({
template: resolve(__dirname, './index.html')
})
],
module: {
rules: [
{
test: /\.(tsx?)|(jsx?)$/,
loader: 'babel-loader',
options: {
presets: [
['babel-preset-react-app', { flow: false, typescript: true }]
]
}
}
]
},
devServer: {
open: 'Google Chrome'
}
}
-
在项目根目录创建一个空白的index.html文件。
-
/src/index.tsx文件:
// /src/index.tsx import React from 'react'; import { render } from 'react-dom'; function App() { return <h1>hello typescript</h1>; } document.write('<div id="root"></div>'); render(<App/>, document.querySelector('#root'));
-
运行:
npx webpack serve
完整的ts项目示例:#
-
安装:
sudo npm i -g xtie-cli
-
创建项目:
xt ts-project
-
运行:
cd ts-project && npm start