TypeScript 是 JS 的超集,强调变量类型。让JS更加接近强类型语言,下图说明了接入 TS 的好处:
1、接入流程
- 安装 tslint 和 typescript
npm i typescript -D
npm install typescript-eslint-parser --save-dev
npm install eslint-plugin-typescript --save-dev // typescript-eslint-parser 对一部分 ESLint 规则支持性不好,故我们需要安装 eslint-plugin-typescript,弥补一些支持性不好的规则
- 项目根目录添加tsconfig.json
// tsconfig.json
{
"compilerOptions": {
"target": "es5", // 编译后的代码转为 es5 以兼容低版本
"module": "esnext", // 读取的文件采用的都是 ES6 的模块系统
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入
"experimentalDecorators": true, // 允许使用 装饰器
"moduleResolution": "node", // 决定如何处理模块
"strict": true, // 启用所有严格检查选项。 包含--noImplicitAny, --noImplicitThis, --alwaysStrict, --strictBindCallApply, --strictNullChecks, --strictFunctionTypes和--strictPropertyInitialization
"allowJs": true, // 接受JavaScript做为输入
"skipLibCheck": true, // 忽略所有的声明文件(*.d.ts)的类型检查
"sourceMap": true,
"importHelpers": true,// 从tslib导入辅助工具函数
"downlevelIteration": true,
"lib": ["es2015", "dom"], // 编译过程中需要引入的库文件的列表
"jsx": "react" // 在.tsx文件里支持JSX
},
"include": [ //读取所有可识别的src目录下的文件
"./src/**/*"
],
"exclude": ["*.js", "dist", "node_modules", "ios", "android"] // 不读取这些文件
}
- 调整 eslintrc 文件
parser: 'typescript-eslint-parser',
plugins: [
'typescript'
],
- 调整 vscode 设置
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact" // jsx 支持
],
- 遇到的一些问题:
1、webpack 中定义了alias后,在文件中使用时报"模块找不到的错误",这时需要在 tsconfig.json 中定义 baseUrl,如
// alias
containers: resolve('src/containers'),
// 某tsx文件中
import Com from 'containers/Com' // 这时会报模块找不到错误
// 配置tsconfig.json 的 baseUrl 可以解决
baseUrl: './src'
2、在 tsx 文件中引入的 scss、css、png等非 js 文件报模块找不到错误,可以在 src 下新建一个 global.d.ts,内容如下:
declare module '*.(scss|png|jpg)' {
const content: any;
export default content;
}
3、webpack 的 module.hot 报错,需安装 @types/webpack-env @types/node
npm i -D @types/webpack-env @types/node
2、基本语法
基础类型
相比于原始JS,多了 any,void,never,元组,枚举
let a:any = 'tom'; // 为编程阶段还不清楚类型的变量指定一个类型
let b:function():void = function(){} // 当一个函数没有返回值时
let c:function():never = function(){ throw Error('error')} // 返回never的函数必须存在无法达到的终点
let d:[string,number] =['tom',123] // 元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同
enum Color = {Green,Red,Blue} // 枚举类型可以为一组数值赋予友好的名字
// 其他:类型断言(即类型转换)
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
// 或者
let strLength: number = (<string>someValue).length;
高级类型
- 交叉类型 &
交叉类型是将多个类型合并为一个类型
function extend<T, U>(first: T, second: U): T & U {
let result = <T & U>{};
for (let id in first) {
(<any>result)[id] = (<any>first)[id];
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
(<any>result)[id] = (<any>second)[id];
}
}
return result;
}
- 联合类型 |
联合类型表示一个值可以是几种类型之一,如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员
function padLeft(value: string, padding: string | number) {
// ...
}
- typeof 和 instanceof 类型保护
TypeScript可以将二者识别为一个类型保护
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
// 类型为SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();
if (padder instanceof SpaceRepeatingPadder) {
padder; // 类型细化为'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
padder; // 类型细化为'StringPadder'
}
// 去除联合类型中的null,语法是添加!后缀:identifier!从identifier的类型里去除了null和undefined
function fixed(name: string | null): string {
function postfix(epithet: string) {
return name!.charAt(0) + '. the ' + epithet; // ok
}
name = name || "Bob";
return postfix("great");
}
- 类型别名 type
类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。通常用类型别名定义联合类型,与接口的区别在于没有产生新的类型,且不能被继承和实现
type a = 'string' | 'number' | null
接口 interface
TypeScript的核心原则之一是对值所具有的结构进行类型检查,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。接口的2个作用:1、类型检查;2、被类继承,强制类实现某种契约
// 基本语法(前 readonly 只读,后 ? 可选属性)
interface 类型名称 {
...
}
- 定义对象接口
interface SquareConfig {
color?: string;
readonly width?: number;
[index: string]: number;
[propName: string]: any;
}
- 定义函数接口
// 函数类型检查器
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
return result > -1;
}
- 定义数组接口
// 数组类型 索引检查器
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0]
- 定义类接口
// 类类型检查器(当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor存在于类的静态部分,所以不在检查的范围内。)
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
// 等价于
declare class 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 PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
- 接口继承类(会继承到类的private和protected成员)
class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}
class Button extends Control implements SelectableControl {
select() { }
}
class TextBox extends Control {
select() { }
}
// Error: Property 'state' is missing in type 'Image'.
class Image implements SelectableControl {
select() { }
}
- 泛型接口
// 函数泛型接口,比普通函数接口更为严格
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
- 混合接口
一个对象可以同时做为函数和对象使用,并带有额外的属性
// 这个接口既可以当类接口也可当对象接口
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
类
- 成员修饰符 public , protected, private
所以成员默认都是public.
private成员不能在类外使用,即不能在实例上以及子类上使用
protected成员可以在子类中使用,不能在自身及其子类的实例上使用。不过可以通过子类的实例方法访问
若把构造函数声明为protected,则该类只能被子类实例化,本身不能实例化 - 只读属性 readonly
只读属性必须在声明时或构造函数里被初始化 - 参数属性
参数属性可以方便地让我们在一个地方定义并初始化一个成员,通过用在构造函数上。参数属性通过给构造函数参数添加一个访问限定符来声明
class Animal {
constructor(private name: string) { }
move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
- 抽象类
abstract 类中的抽象方法必须被子类所实现
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log('roaming the earch...');
}
}
函数
传递给一个函数的参数个数必须与函数期望的参数个数一致
- 可选参数和默认参数
可选参数必须跟在必须参数后面
带默认值的参数不需要放在必须参数的后面。 如果带默认值的参数出现在必须参数前面,用户必须明确的传入undefined值来获得默认值
function buildName(firstName: string, lastName?: string) {
if (lastName)
return firstName + " " + lastName;
else
return firstName;
}
function buildName(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}
- 剩余参数
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;
- 函数重载
为同一个函数提供多个函数类型定义来进行函数重载
function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
// Check to see if we're working with an object/array
// if so, they gave us the deck and we'll pick the card
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// Otherwise just let them pick the card
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}
泛型
泛型即类型变量,它是一种特殊的变量,只用于表示类型而不是值
function identity<T>(arg: T): T {
return arg;
}
我们定义了泛型函数后,可以用两种方法使用。
第一种是,传入所有的参数,包含类型参数:
let output = identity<string>("myString"); // type of output will be 'string'
第二种方法更普遍。利用了类型推论 -- 即编译器会根据传入的参数自动地帮助我们确定T的类型:
let output = identity("myString"); // type of output will be 'string'
如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。
- 泛型接口
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
- 泛型类
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
- 泛型约束
在泛型中使用 extends 关键字
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
类型兼容性
- 普通类型和对象
要将y复制给x,必须保证x的每个属性都在y中,且类型匹配。对于y多余的属性则不管。
interface Named {
name: string;
}
let x: Named;
let y = { name: 'Alice', location: 'Seattle' };
x = y;
- 函数兼容
与上面刚好相反
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // OK
x = y; // Error
模块
模块的导入导出和ES6一致,import export,但也有新增部分
- 整体导出
使用export =导出一个模块,则必须使用TypeScript的特定语法import module = require("module")来导入此模块。
let numberRegexp = /^[0-9]+$/;
class ZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export = ZipCodeValidator;
// 导入
import zip = require("./ZipCodeValidator");
- 使用外部模块
node里大部分模块都不是TS写的,如果要用就需要为这些模块做个TS声明,一般放在.d.ts文件里:
// node.d.ts
declare module "url" {
export interface Url {
protocol?: string;
hostname?: string;
pathname?: string;
}
export function parse(urlStr: string, parseQueryString?, slashesDenoteHost?): Url;
}
declare module "path" {
export function normalize(p: string): string;
export function join(...paths: any[]): string;
export let sep: string;
}
现在我们可以/// <reference> node.d.ts并且使用import url = require("url");或import * as URL from "url"加载模块。
/// <reference path="node.d.ts"/>
import * as URL from "url";
let myUrl = URL.parse("http://www.typescriptlang.org");
假如你不想在使用一个新模块之前花时间去编写声明,你可以采用声明的简写形式以便能够快速使用它。
declare module "hot-new-module";
import x, {y} from "hot-new-module"; // 该模块的导出所有类型都将会是any
x(y);
- 导入其他类型文件,如txt,json等
declare module "*!text" {
const content: string;
export default content;
}
// Some do it the other way around.
declare module "json!*" {
const value: any;
export default value;
}
//
import fileContent from "./xyz.txt!text";
import data from "json!http://example.com/data.json";
console.log(data, fileContent);
- 扩展模块
// map.ts
import { Observable } from "./observable";
declare module "./observable" {
interface Observable<T> {
map<U>(f: (x: T) => U): Observable<U>;
}
}
Observable.prototype.map = function (f) {
// ... another exercise for the reader
}
// consumer.ts
import { Observable } from "./observable";
import "./map";
let o: Observable<number>;
o.map(x => x.toFixed());
// observable.ts
export class Observable<T> {
// ... still no implementation ...
}
declare global { // 全局扩展
interface Array<T> {
toObservable(): Observable<T>;
}
}
Array.prototype.toObservable = function () {
// ...
}
一些操作符和关键字
- keyof T 索引类型查询操作符
keyof T的结果为T上已知的公共属性名的联合 - T[K] 索引访问操作符
T[K] 返回属性的类型 - extend 继承
接口继承接口,接口继承类,类继承类 - implement 实现
类实现接口 - extends 泛型约束关键字
- new
function createInstance<A>(c: new () => A): A {
return new c();
}
function createInstance<A>(c: {new(): T; }): A {
return new c();
}
常见问题
- 枚举类型key-value互转
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green; // 2
let colorName: string = Color[Color.Green]; // 'Green'
- 给回调函数声明类型
type Cb = () => void;
on(type: string, cb: Cb);
- 自己写 类型声明文件
// 首先声明一下模块:
declare module 'progressbar.js' {
// 模块中暴露了 Circle 类
export class Circle {
constructor(container: HTMLElement, options: Options);
}
// 构造函数的 Options 需要单独声明
interface Options {
easing?: string;
strokeWidth?: number;
trailColor?: string;
trailWidth?: number;
}
}
- 接口属性定义顺序
// 只读、必选、可选、未知属性
interface iProps {
readonly x: number;
readonly y: number;
name: string;
age: number;
height?: number;
[propName: string]: any;
}
- 小技巧
// 定义一个javascript的对象,key是字符串,value是任意类型
const people:Record<string,any> = {
name: 'chengfeng',
age: 10
}
// 将传入的属性变为可选项
interface IPeople {
title: string;
name: string;
}
const people: Partial<IPeople> = {
title: 'Delete inactive users',
};
// 传入的属性变为变成只读
const people: Readonly<IPeople> = {
title: 'todo list',
name: chenfeng;
};
const people1: Partial<IPeople> = { title: 'ts' }; // OK
const people22: Required<IPeople> = { title: 'ts' }; // Error: property 'name' missing
type T = keyof IPeople // -> "name" | "age"
// 结合 react-router-dom 使用
import { withRouter, RouteComponentProps } from 'react-router-dom';
class App extends React.Component<IProps & RouteComponentProps<{}>, AppStates> {}
export default withRouter(App);
- any 与 object 类型的区别
Object类型的变量只是允许你给它赋任意值,但是却不能够在它上面调用任意的方法,即便它真的有这些方法;any 可以 - readonly 与 const
做为变量使用的话用const,若做为属性则使用readonly - 对象字面量的TS检测
对象字面量会被特殊对待而且会经过额外属性检查,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。可以用索引签名解决这个问题:
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any; // 可包含任何属性
}
- 类作为接口
类可以产生接口,类分为静态部分和实例部分
class Clock implements ClockInterface {
currentTime = new Date()
setTime(d: Date) {
this.currentTime = d
}
reset() {}
a = 1
constructor(h: number, m: number) {}
}
let clock: typeof Clock = Clock // typeof Clock 取得是 构造函数 的类型
3、React 中使用的最佳实践
- vscode 安装 Typescript React code snippets 插件
- 所有用到 jsx 语法的文件都需要以 tsx 后缀命名
- 使用组件声明时的Component<P, S>泛型参数声明,来代替PropTypes!
- 全局变量或者自定义的 window 对象属性,统一在项目根下的 global.d.ts 中进行声明定义
- 对于项目中常用到的接口数据对象,在schemas/目录下定义好其结构化类型声明
- 类组件的声明
class App extends Component<IProps, IState> {
static defaultProps = {
// ...
}
readonly state = { // 使用 class properties 语法对state做初始化时,会覆盖掉Component<P, S>中对state的readonly标识,所以需要显示写上 readonly
// ...
};
// 小技巧:如果state很复杂不想一个个都初始化,可以结合类型断言初始化state为空对象或者只包含少数必须的值的对象: readonly state = {} as IState;
}
- 函数式组件的声明
// SFC: stateless function components
// v16.7起,由于hooks的加入,函数式组件也可以使用state,所以这个命名不准确。新的react声明文件里,也定义了React.FC类型^_^
const List: React.SFC<IProps> = props => null
- 正确的声明高阶组件
import { RouteComponentProps } from 'react-router-dom';
// 方法一
@withRouter
class App extends Component<Partial<RouteComponentProps>> {
public componentDidMount() {
// 这里就需要使用非空类型断言了
this.props.history!.push('/');
}
// ...
});
// 方法二
@withRouter
class App extends Component<{}> {
get injected() {
return this.props as RouteComponentProps
}
public componentDidMount() {
this.injected.history.push('/');
}
// ...
interface IVisible {
visible: boolean;
}
//排除 IVisible
function withVisible<Self>(WrappedComponent: React.ComponentType<Self & IVisible>): React.ComponentType<Omit<Self, 'visible'>> {
return class extends Component<Self> {
render() {
return <WrappedComponent {...this.props} visible={true} />
}
}
}
- react-router-dom 中常用 RouteComponentProps,react中常用的 ReactNode
// IProps 常用写法
interface IProps extends RouteComponentProps {
title: string | ReactNode
style: any
}
// 或者
type IProps = {
style: any
config: {}[]
}
// IState 状态声明常用写法
const defaultState = {$1}
type IState = Readonly<typeof defaultState>
- 在组件定义的地方定义接口,而不是使用的时候,否则需要定义两遍
// 定义处
type IProps = {
style?: any
config: {}[]
}
export default class Paragraph extends PureComponent<IProps, {}> {
render() {
const { style, config } = this.props
return (
<div className={styles.layout} style={style}>
{config.map((item, index) => (
<p className={styles.paragraph} key={index} style={style}>
{item}
</p>
))}
</div>
)
}
}
// 使用处
<Paragraph config={this.config.subTitle} />
- 泛型IProps
上述IProps中,如果config是多类型的对象数组,则可以这样
type IProps<T> = {
style?: any
config: T[]
}
export default class Paragraph<T> extends PureComponent<IProps<T>, {}> {
render() {
const { style, config } = this.props
return (
<div className={styles.layout} style={style}>
{(config as T[]).map((item, index) => (
<p className={styles.paragraph} key={index} style={style}>
{item}
</p>
))}
</div>
)
}
}
参考
https://github.com/plantain-00/blogs/issues/28
https://tasaid.com/blog/20171102225101.html
https://juejin.im/entry/5a156adaf265da43231aa032
typescript 高级技巧
https://mp.weixin.qq.com/s/_lO3cd0FcF0Dg3TRnHPdwg)
TypeScript 模块解析