怎么上... 咳咳,你们别想歪,这是一篇纯技♂术文章。javascript
什么?尤大要把Vue 3.0所有改为用Typescript来写?这不是逗我吗,那我是否是要用TypeScript来写Vue应用了? html
好吧,Vue3.0可能最快也要19年年底才出来,Vue3.0是会对Ts使用者更友好,而不是只能用ts了,尤大使用ts的缘由也是由于ts的静态类型检测以及ts的表现比flow愈来愈好了。自从巨硬大步迈向开源,前端圈子多了不少新工具好比VS Code、TypeScript。我的认为TypeScript真正火起来仍是由于前端应用的复杂度不断飙升,这带来的问题就是维护性以及扩展性会变差。尤为在编写类库的时候,更是须要考虑各个类以及方法的复用性和扩展性,因此会使用到设计模式来优化代码。还有更重要的就是,编码效率的提升,静态系统无疑是下降了调试bug的时间。前端
优势java
缺点node
let foo = 123; foo = '456'; // Error: cannot assign `string` to `number
Webpack已经发布到版本4.41了,相信不少小伙伴已经上了webpack4了,Webpack4对typescript的支持也是8错的,它最大的变化莫过于"零配置"以及将commonChunks plugin插件嵌入为webpack内置。最新版本:webpack
npm install -g typescript
假设咱们有一个用TypeScript编写的Student类。git
class Student { private name: string; constructor(name: string) { this.name = name; } }
使用typescript compiler来编译它程序员
tsc student.ts
编译后的结果是根据编译选项来生成的标准JavaScript文件。es6
var Student = /** @class */ (function () { function Student(name) { this.name = name; } return Student; }());
TypeScript-->ES Next的Javascript版本-->兼容性较好的JavaScript。
以前已经安装了TypeScript compiler,一般会在compiler option中指定typescript是要编译到支持ES5/ES6/ES Next的JavaScript版本,可是在实践中咱们还须要利用Babel这个结果再进行一次转译,这么作的缘由有两个。github
const path = require('path') const webpack = require('webpack') const config = { entry: './src/index.ts', module: { rules: [ { // ts-loader: convert typescript to javascript(esnext), // babel-loader: converts javascript(esnext) to javascript(backward compatibility) test: /\.(tsx|ts)?$/, use: ['babel-loader', 'ts-loader'], exclude: /node_modules/ }, ] }, resolve: { extensions: ['.tsx', '.ts', '.js'], alias: { '@': path.resolve(__dirname, './src'), 'mobx': path.resolve(__dirname, './node_modules/mobx/lib/mobx.es6.js') } }, }
简单介绍一下typescript的编译选项,一般会在这里指定编译目标JS版本,代码的模块化方式以及代码的检查规则等。
allowJS
表示是否容许编译JavaScript文件。target
表示ECMAScript目标版本,好比‘ESNext’、'ES2015'。module
表示模块化的方式,好比'commonjs'、'umd'或'es2105'(es module)moduleResolution
表示的是模块解析的策略,即告诉编译器在哪里找到当前模块,指定为'node'时,就采用nodejs的模块解析策略,完整算法能够在Node.js module documentation找到;当它的值指定为'classic'时则采用TypeScript默认的解析策略,这种策略主要是为了兼容旧版本的typescript。strict
是否启动全部的严格类型检查选型,包括'noImplicitAny','noImplicitThis'等。lib
表示编译过程当中须要引入的库文件的列表,根据实际应用场景来引入。experimentalDecorators
是为了支持装饰器语法的选项,由于在项目中使用了Mobx作状态管理,因此须要启用装饰器语法。include
选项表示编译的目录outDir
表示编译结果输出的目录。{ "compileOnSave": true, "compilerOptions": { "target": "esnext", "module": "esnext", "moduleResolution": "node", "sourceMap": true, "strict": true, "allowJs": true, "experimentalDecorators": true, "outDir": "./dist/", "lib": [ "es2015", "dom", "es2016", "es2017", "dom.iterable", "scripthost", "webworker" ] }, "include": [ "src/**/*.ts" ] }
tslint是针对typescript的lint工具,相似eslint遵循Airbnb Style或Standard Style,eslint也能够指定要遵循的typescript规范,目前在tslint官方,给出了三种内置预设,recommended
、latest
以及all
,省去了咱们去对tslint每条规则进行配置的麻烦。
recommended
是稳定版的规则集,通常的typescript项目中使用它比较好,遵循SemVer。latest
会不断更新以包含每一个TSLint版本中最新规则的配置,一旦TSLint发布了break change,这个配置也会跟随着一块儿更新。all
将全部规则配置为最为严格的配置。tslint规则
tslint的规则是有严重性等级的划分,每条规则能够配置default
error
warning
或off
。tslint预设提供了不少在代码实践中提炼出来的规则,我认为有下面若干的规则,咱们会常常遇到,或者须要关注一下。
only-arrow-functions
只容许使用箭头函数,不容许传统的函数表达式。promise-function-async
任何返回promise的函数或方法,都应该使用'async'标识出来;await-promise
在'await'关键字后面跟随的值不是promise时会警告,规范咱们异步代码的编写。no-console
禁止在代码中使用'console'方法,便于去除无用的调试代码。no-debugger
禁止在代码中使用'debugger'方法,同上。no-shadowed-variable
当在局部做用域和外层做用域存在同名的变量时,称为shadowing,这会致使局部做用域会没法访问外层做用域中的同名变量。no-unused-variable
不容许存在,未使用的变量、import或函数等。这个规则的意义在于避免编译错误,同时由于声明了变量却不适用,也致使了读者混淆。max-line-length
要求每行的字数有限制;quotemark
指定对字符串常量,使用的符号,通常指定'single';这个看团队风格了。prefer-const
尽量用'const'声明变量,而不是'let',不会被重复赋值的变量,默认使用'const';其余规则你们能够详细看tslint官方文档,使用lint能够更好地规范代码风格,保持团队代码风格的统一,避免容易致使编译错误的问题以及提升可读性和维护性。
tslint的特殊flags
咱们用ts写代码的时候,常常会遇到一行代码的字数过长的状况,此时可使用tslint提供的flag来使得该行不受规则的约束。
// tslint:disable-next-line:max-line-length private paintPopupWithFade<T extends THREE.Object3D>(paintObj: T, popupStyleoption: PopupStyleOption, userDataType: number) { //... }
实际上,tslint提示是该行的字数违反了 max-line-length规则,此处能够经过增长注释 // tslint: disable-next-line: rulex
来禁用这个规则。
"鸭子"类型??(黑人问号), 第一次看到这名词我也很懵逼, 其实它说的是结构型类型,而目前类型检测主要分为结构型(structural)类型以及名义型(nominal)类型。
interface Point2D { x: number; y: number; } interface Point3D { x: number; y: number; z: number; } var point2D: Point2D = { x:0, y: 10} var point3D: Point3D = { x: 0, y: 10, z: 20} function iTakePoint2D(point: Point2D) { /*do sth*/ } iTakePoint2D(point2D); // 类型匹配 iTakePoint2D(point3D); // 类型兼容,结构类型 iTakePoint2D({ x:0 }); // 错误: missing information `y`
区别
知道了typescript是个'鸭子类型'后,咱们就会想到一个问题,ts这种鸭子类型
怎么判断类型啊,好比下面这个例子:
public convertString2Image(customizeData: UserDataType) { if (Helper.isUserData(customizeData)) { const errorIcon = searchImageByName(this.iconImage, statusIconKey); if (errorIcon) { (customizeData as UserData).title.icon = errorIcon; } } else if (Helper.isUserFloorData(customizeData)) { // do nothing } else { // UserAlertData let targetImg; const titleIcon = (customizeData as UserAlertData)!.title.icon; if (targetImg) { (customizeData as UserAlertData).title.icon = targetImg; } } return customizeData; }
该方法是根据传入的用户数据来将传入的icon字段用实际对应的图片填充,customizeData
是用户数据,此时咱们须要根据不一样类型来调用searchImageByName
方法去加载对应的图片,因此咱们此时须要经过一些类型判断的方法在运行时判断出该对象的类型。
基础的类型判断
基本的类型判断方法咱们可能会想到typeof
和instanceof
,在ts中,其实也可使用这两个操做符来判断类型,好比:
typeof
判断类型function doSomething(x: number | string) { if(typeof x === 'string') { console.log(x.toFixed()); // Property 'toFixed' does not exist on type 'string' console.log(x.substr(1)); } else if (typeof x === 'number') { console.log(x.toFixed()); console.log(x.substr(1)); // Property 'substr' does not exist on type 'number'. } }
能够看到使用typeof
在运行时判断基础数据类型是可行的,能够在不一样的条件块中针对不一样的类型执行不一样的业务逻辑,可是对于Class
或者Interface
定义的非基础类型,就必须考虑其余方式了。
instanceof
判断类型下面这个例子根据传入的geo
对象的类型执行不一样的处理逻辑:
public addTo(geo: IMap | IArea | Marker) { this.gisObj = geo; this.container = this.draw()!; if (!this.container) { return; } this.mapContainer.appendChild<HTMLDivElement>(this.container!); if (this.gisObj instanceof IMap) { this.handleDuration(); } else if(this.gisObj instanceof Marker) { // } }
能够看到,使用instanceof
动态地判断类型是可行的,并且类型能够是Class
关键字声明的类型,这些类型都拥有复杂的结构,并且拥有构造函数。总地来讲,使用instanceof
判断类型的两个条件是:
prototype
属性类型不能为any
。利用类型谓词来判断类型
结合一开始的例子,咱们要去判断一个鸭子类型,在ts中,咱们有特殊的方式,就是类型谓词
(type predicate)的概念,这是typescript的类型保护机制,它会在运行时检查确保在特定做用域内的类型。针对那些Interface
定义的类型以及映射出来的类型,并且它并不具备构造函数,因此咱们须要本身去定义该类型的检查方法,一般也被称为类型保护
。
例子中的调用的两个基于类型保护的方法的实现
public static isUserData(userData: UserDataType): userData is UserData { return ((userData as UserData).title !== undefined) && ((userData as UserData).subTitle !== undefined) && ((userData as UserData).body !== undefined) && ((userData as UserData).type === USER_DATA_TYPE.USER_DATA); } public static isUserFloorData(userFloorData: UserDataType): userFloorData is UserFloorData { return ((userFloorData as UserFloorData).deviceAllNum !== undefined) && ((userFloorData as UserFloorData).deviceNormalNum !== undefined) && ((userFloorData as UserFloorData).deviceFaultNum !== undefined) && ((userFloorData as UserFloorData).deviceOfflineNum !== undefined); }
实际上,咱们要去判断这个类型的结构,这也是为何ts的类型系统被称为鸭子类型
,咱们须要遍历对象的每个属性来区分类型。换句话说,若是定义了两个结构彻底相同的类型,即使类型名不一样也会判断为相同的类型~
索引类型(index types),使用索引类型,编译器就可以检查使用了动态属性名的代码。ts中经过索引访问操做符keyof
获取类型中的属性名,好比下面的例子:
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] { return names.map(n => o[n]); } interface Person { name: string; age: number; } let person: Person { name: 'Jarid', age: 35 } let strings: string[] = pluck(person, ['name']);
原理
编译器会检查name
是否真的为person
的一个属性,而后keyof T
,索引类型查询操做符,对于任何类型T, keyof T
的结果为T上已知的属性名的联合。
let personProps: keyof Person; // 'name' | 'age'
也就是说,属性名也能够是任意的interface类型!
索引访问操做符T[K]
索引类型指的其实ts中的属性能够是动态类型,在运行时求值时才知道类型。你能够在普通的上下文中使用T[K]
类型,只须要确保K extends keyof T
便可,例以下面:
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] { return o[name]; }
原理:o:T
和 name:K
表示o[name]: T[K]
当你返回T[K]
的结果,编译器会实例化key的真实类型,所以getProperty的返回值的类型会随着你须要的属性改变而改变。
let name: string = getProperty(person, 'name'); let age: number = getProperty(person, 'age'); let unknown = getProperty(person, 'unknown'); // error, 'unknown' is not in 'name' | 'age'
索引类型和字符串索引签名keyof
和 T[k]
与字符串索引签名进行交互。
好比:
interface Map<T> { [key: string]: T; // 这是一个带有字符串索引签名的类型, keyof T 是 string } let keys: keyof Map<number>; // string let value: Map<number>['foo']; // number
Map<T>
是一个带有字符串索引签名的类型,那么keyof T 会是string。
背景
在使用typescript时,会有一个问题咱们是绕不开的 --> 如何从旧的类型中建立新类型即映射类型。
interface PersonPartial { name?: string; age?: number; } interface PersonReadonly { readonly name: string; readonly age: number; }
能够看到PersonReadOnly
这个类型仅仅是对PersonParial
类型的字段只读化设置,想象一下 若是这个类型是10个字段那就须要重复写这10个字段。咱们有没办法不去重复写这种样板代码,而是经过映射获得新类型? 答案就是映射类型,
映射类型的原理
新类型以相同的形式去转换旧类型里每一个属性:
type Readonly<T> { readonly [P in keyof T]: T[P]; }
它的语法相似于索引签名的语法,有三个步骤:
Keys
,包含了要迭代的属性名的集合好比下面这个例子
type Keys = 'option1' | 'option2'; type Flags = { [K in keys]: boolean };
Keys
,是硬编码的一串属性名,而后这个属性的类型是boolean,所以这个映射类型等同于:
type Flags = { option1: boolean; option2: boolean; }
典型用法
咱们常常会遇到的或者更通用的是(泛型的写法):
type Nullable<T> = { [P in keyof T]: T[P] | null }
声明一个Person类型,一旦用Nullable类型转换后,获得的新类型的每个属性就是容许为null的类型了。
// test interface Person { name: string; age: number; greatOrNot: boolean; } type NullPerson = Nullable<Person>; const nullPerson: NullPerson = { name: '123', age: null, greatOrNot: true, };
骚操做
利用类型映射,咱们能够作到对类型的Pick
和Omit
,Pick
是ts自带的类型,好比下面的例子:
export interface Product { id: string; name: string; price: string; description: string; author: string; authorLink: string; } export type ProductPhotoProps = Pick<Product, 'id' | 'author'| 'authorlink' | 'price'>; // Omit的实现 export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; export type ProductPhotoOtherProps = Omit<Product, 'name' | 'description'>;
咱们能够把已有的Product
类型中的若干类型pick
出来组成一个新类型;也能够把若干的类型忽略掉,把剩余的属性组成新的类型。
好处
never
首先,never
类型有两种场景:
// 返回never的函数必须存在没法达到的终点 function error(message: string): never { throw new Error(message); } // 推断的返回值类型为never function fail() { return error("Something failed"); }
voidvoid
也有它的应用场景
void
。void
类型或者返回值标记为void
能够提升代码的可读性,让人明确该方法是不会有返回值,写测试时也能够避免去关注返回值。public remove(): void { if (this.container) { this.mapContainer.removeChild(this.container); } this.container = null; }
小结
never
实质表示的是那些永远不存在值的类型,也能够表示函数表达式或箭头函数表达式的返回值。void
类型,变量仍然能够被赋值undefined
或null
,可是never
是只能被返回值为never
的函数赋值。ts中用enum
关键字来定义枚举类型,彷佛在不少强类型语言中都有枚举的存在,然而Javascrip没有,枚举能够帮助咱们更好地用有意义的命名去取代那些代码中常常出现的magic number
或有特定意义的值。这里有个在咱们的业务里用到的枚举类型:
export enum GEO_LEVEL { NATION = 1, PROVINCE = 2, CITY = 3, DISTRICT = 4, BUILDING = 6, FLOOR = 7, ROOM = 8, POINT = 9, }
由于值都是number
,通常也被称为数值型枚举。
基于数值的枚举
ts的枚举都是基于数值类型的,数值能够被赋值到枚举好比:
enum Color { Red, Green, Blue } var col = Color.Red; col = 0; // 与Color.Red的效果同样
ts内部实现
咱们看看上面的枚举值为数值类型的枚举类型会怎样被转为JavaScript:
// 转译后的Javascript define(["require", "exports"], function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var GEO_LEVEL; (function (GEO_LEVEL) { GEO_LEVEL[GEO_LEVEL["NATION"] = 1] = "NATION"; GEO_LEVEL[GEO_LEVEL["PROVINCE"] = 2] = "PROVINCE"; GEO_LEVEL[GEO_LEVEL["CITY"] = 3] = "CITY"; GEO_LEVEL[GEO_LEVEL["DISTRICT"] = 4] = "DISTRICT"; GEO_LEVEL[GEO_LEVEL["BUILDING"] = 6] = "BUILDING"; GEO_LEVEL[GEO_LEVEL["FLOOR"] = 7] = "FLOOR"; GEO_LEVEL[GEO_LEVEL["ROOM"] = 8] = "ROOM"; GEO_LEVEL[GEO_LEVEL["POINT"] = 9] = "POINT"; })(GEO_LEVEL = exports.GEO_LEVEL || (exports.GEO_LEVEL = {})); });
很是有趣,咱们先不去想为何要这么转译,换个角度思考,其实上面的代码说明了这样一个事情:
console.log(GEO_LEVEL[1]); // 'NATION' console.log(GEO_LEVEL['NATION']) // 1 // GEO_LEVEL[GEO_LEVEL.NATION] === GEO_LEVEL[1]
因此其实咱们能够经过这个枚举变量GEO_LEVEL去将下标表示的枚举转为key
表示的枚举,key
表示的枚举也能够转为用下标表示。