本文不会详细去介绍TypeScript的基础语法和用法,对TypeScript还不熟悉的同学可以先走马观花的浏览一下相关的文档:
几个关键词:类型声明、类、接口、泛型。
我使用的IDE是VSCode,其内置了TypeScript解释器。其他常用的编辑器都可以通过插件的方式提供对TypeScript的支持。
对于项目,现在大家应该都使用webpack打包,所以安装一下必要的依赖:
nenpm install --save-dev typescript awesome-typescript-loader source-map-loader
然后在webpack添加一些配置项,关键部分:
resolve: {
extensions: [".ts", ".tsx", ".js", ".json"]
},
module : {
rules : [{
test: /\.tsx?$/,
loader: "awesome-typescript-loader"
}, {
enforce: "pre",
test: /\.js$/,
loader: "source-map-loader"
},
]
}
纯使用TypeScript的话,就没有必要再去使用Babel了。
大部分编辑器可以安装tslint
的插件,这是一个和eslint
类似并且可以结合TypeScript语法规则进行更有效的规范约定的工具。
通常可以在tslint中继承标准规范:
{
"extends": "tslint:recommended",
"rules": {
/* 你的自定义规则 */
}
}
然后结合具体开发习惯进行规则的修改。
TSLint的全部规则可以在这里查阅到。
常规情况下,我们如果想安装第三方类库(比如lodash
)的时候,通常直接如下一个命令即可:
nenpm i lodash --save
但是默认情况下,第三方类库都不会提供.d.ts
文件来声明这个模块的属性方法,也就是说我们无法在编辑器中享受到TypeScript带来的类型检查。
安装声明文件的方式有tsd
、typings
和@types
,其中前两种已经不推荐了,这里直接使用@types
方式安装。
只需变更两个地方: 在原来安装的包名前加上@types/
。并且换成--save-dev
即可。
nenpm i @types/lodash --save-dev
通常绝大部分流行的第三方库都提供了声明文件,可以在http://npm.hz.netease.com/browse/keyword/@types这里浏览和搜索。
第一步
首先需要安装@types
文件,通常:
nenpm i react --save
nenpm i react-dom --save
nenpm i @types/react --save-dev
nenpm i @types/react-dom --save-dev
第二步
引入React和React DOM的方式如下:
import * as React from 'react';
import * as ReactDOM from 'react-dom';
为什么要这样引入呢, 因为在@types/react
的声明文件中, 使用了如下方式进行模块导出:
export = React
这个是在早期Typescript综合CommonJS和AMD提出的导出模块方式,现在ES6出现之后,大家基本都统一了,不过考虑到历史原因和兼容性,基本上所有类库还是以这样的形式导出。这种在ES6中不算默认导出的,所以不能直接以默认导入的方式引进来。
对于此种导出方式,官方给出的导入方式是:
import React = require('react');
是不是有点不伦不类?所以也可以按照TypeScript提供的ES6语法全部导入到一个对象。
import * as React from 'react';
第三步
泛型可以说是TypeScript一个很大的亮点,它可以为我们定义React Component的State和Props类型。
// TypeScript建议Interface类型名称都以I开头
interface IState {
name: string;
age?: number;
}
interface IProps {
score: number;
}
class Student extends React.Component<IProps, IState> {
// ......
}
至此,在这个component内部访问props和state的都有了类型检查的保障。
如果不想进行类型检查,可以使用any
替换IProps
或者IState
。
另外进行类型强转的时候,请不要使用类似这种形式的,因为会被误认为jsx标签...
备注: React的生命周期访问修饰符请使用public。因为这些钩子函数是被React框架主动调用的,可以类比于Android和iOS开发的生命周期函数。
TypeScript已经尽力做到和未来的ES规范保持一致,基本上ES有的功能,TS都有,所以可以尽管放心食用。
在webpack
中使用awesome-typescript-loader
之后,整体编译的配置项就交给了tsconfig.json
,这个文件需要放在项目的最外层。这里只列举最通用的配置项,详细的可以在官方手册中查询。
通常只会用到几个配置主项:
compilerOptions: object // 编译设置,下面会重点说明。
include: string[] // 包含目录,如 "src/**/*" 就是指src目录下所有ts相关文件
exclude: string[] // 不编译目录,也会过滤include中的目录
extends: string // 可以继承别的tsconfig文件,填写继承地址
下面说一下compilerOptions
相关的子配置项(希望大家看得懂我的描述方式):
allowJs: boolean = false
allowJs
允许在ts中编译引入的js文件,通常建议你一旦启用了ts,可以使用any类型, 但是不要出现任何.js
或者.jsx
文件,所以此项最好设置为false
。
baseUrl: string
baseUrl
用于处理项目中的非相对路径,比如指定为"baseUrl":"./src/util"
,在你的任意文件中import Common from 'common'
,则编译器会优先去查找根目录下的./src/util/common
文件。
paths: <custom object>
paths
需要搭配baseUrl使用,类似webpack的aliash功能,举例来说,设置了paths: { c: ['common'] }
之后,则可以直接使用import Common from 'c'
的形式。数组是表示按优先级查找的顺序。
experimentalDecorators: boolean = false
experimentalDecorators
允许开启类似ES提案中的装饰器功能,用法几乎一模一样。
jsx: 'preserve' | 'react' = 'preserve'
jsx
这个配置项去处理如何解析tsx
文件,preserve
是只转换成jsx
文件,后续操作交给比如babel
之类的loader,而react
则是直接在tsloader这层完成解析的操作,建议改成react
。
lib: string[]
lib
当我们需要在ts中使用一些高级的API,比如Proxy
,Reflect
甚至Element.prototype.matches(DOM API)
,我们需要引入TypeScript的内置库,比如es2015
, dom
等等。
outDir: string
outDir
最终生成代码输出目录
sourceMap: boolean = false
sourceMap
是否生成sourceMap,建议开启,方便调试。
target: 'ES3' | 'ES5' | 'ES2016'... = 'ES3'
target
生成代码支持的ES版本,通常建议使用ES5
以兼容浏览器。
module: 'commonjs' | 'ES6'
module
最终模块生成方式,如果target
是ES5的话就使用CommonJS。
随手附上一个最基本的tsconfig.json配置:
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"experimentalDecorators": true,
"lib": ["es2017", "dom"],
"module": "commonjs",
"target": "es5",
"jsx": "react",
"baseUrl": "./src/"
},
"include": [
"./src/**/*"
]
}
Q1. TypeScript声明的类型,一定要严格匹配吗?
答: TypeScript的引用类型赋值是向下兼容的,举例来说:
interface IFoo {
name: string;
}
const bar = { name: 'foo', age: 17 };
const foo: IFoo = bar;
这段代码完全可以通过编译,只要bar对象拥有string类型的属性name即可。
并且,你只能访问foo的name属性,无法访问age属性
不过,你不能直接给foo赋不合法的对象字面值,像这样:
const foo: IFoo = { name: 'foo', age: 19 }
这样是无法通过编译的。
Q2. 是否必须严格定义每一个对象的类型?
答: 我个人感觉是没有必要的,当你不进行类型声明的时候,类型就是any。
如果一定要严格类型检查,可以在tsconfig中开启noImplicitAny
Q3. 如何声明一个可以自定义属性的object?
答:如果你在定义对象的时候,以这种方式定义:
const foo: {} = {};
或者 const foo: object = {};
则你永远也不能往foo里面增添属性,除非转为any,但这就意味着这连一个对象都不是了。
我的解法是在全局定义一个AnyObject类型:
class AnyObject {
[propName: string]: any;
}
这样定义自定义对象的时候就可以如下使用(注意要先引入AnyObject):
const myObj: AnyObject = {/* balabala */}
Q4. 我要如何给window
添加自定义属性?
答:尽管现在直接给window添加属性已经非常不推荐了,不过还是有可能会有这样的情况。
解决办法其实跟上面非常相似,自定义一个window类型。
class MyWindow extends Window {
public global: string;
}
然后就可以这样访问了:
const global = (window as MyWindow).global;
Q5. 接口和类的区别是什么?
答: TypeScript有一个很灵活的点就是接口可以继承类,这在某种程度上造成了我的困扰......
类:
类的含义大家都理解,TypeScript新增的访问修饰符,private只在内部访问,protected只允许内部和子类访问,public允许全局访问,没有包访问修饰。
也可以使用abstract来修饰类,抽象类一定至少需要一个抽象方法。
接口:
接口用来描述共性特征,可以定义对象有哪些属性和方法。其实类型声明就是接口类型的声明。
接口没有办法指定默认值。
当然,接口里面是无法实现方法的,并且所有的成员都是public属性。
下面来说一下令人头疼的继承关系:
1. 类继承类,单继承,extends
这个应该很常见,是继承的最基本实现,ES6中就已经支持,不再赘述了。
2. 接口继承接口,单继承,extends
接口之间的继承和类非常相似,所有的成员都会继承过来。
3. 类实现接口,多继承,implements
这个也比较常见,举个例子,
class C implements IA, IB {}
则 C 中一定要定义接口声明的类型和方法,并且修饰符设置为public属性。
特别的,如果IA、IB中有同名属性,则属性类型必须要一致,这个也很合理。
4. 接口继承(?)类,单继承,extends
这种情况非常复杂,因为你还需要考虑到继承完类的接口再被实现的问题。
最后我得出的总结是:
接口继承类只适合于修饰符全为public的类,其他修饰符都会使这个接口变得不纯净,不推荐使用。
我个人认为在项目中启用TypeScript是一件很需要魄力的事情,因为这门语言会对习惯了弱类型的开发者带来很大的冲击。
这里不去比较弱类型和强类型的孰优孰劣,只说引用TypeScript的好处:
个人想法,在以下情况下适合用TypeScript开发:
TypeScript不会像CoffeeScript一样快来快退,它有着非常明显的不可替代性。至于是否在项目中启用TypeScript,现在也没有绝对的答案,毕竟工具的诞生都是为了提高效率。
网易云新用户大礼包:https://www.163yun.com/gift
本文来自网易实践者社区,经作者高臻熙授权发布。