为什么使用TypeScript?
日常开发中弱类型JavaScript的痛点
- 引用的组件/函数不知道可接收的参数以及参数类型-----各种找文档----甚至深入到源码
- 复杂数据的数据流转难以追踪----各种Debugger或者日志排查
- BFF/后端接口字段以及字段类型不明确----各种找文档----负责人
- 底层依赖的接口类型改动----前端全局搜索替换改动的地方----调试
TypeScript为了类型定义而诞生,具有以下优势
- 定义组件的属性以及函数的参数,代码即文档展示对应的类型
- 对复杂数据定义类型,在数据流转时也能清晰的知道数据类型,便于追踪
- 对后端接口,规范定义类型,更易于维护
- 静态类型检查,在coding阶段发现问题
- 强大的IDE自动补全/检查
带来的收益和成本
- 收益相关:问题提前暴露、复杂数据流转追踪、IDE的智能提示、强大的Type系统
- 成本相关: 增加学习和类型维护成本
TypeScript in React
开发环境: ESLint+Prettier+TypeScript Playground with React
ESLint
- ESLint --Javascript Lint,规范代码质量,提供开发效率
- 安装依赖
- Eslint: Javascript 代码检测工具
- @typescript-eslint/eslint-plugin:TS规则列表,可以打开或关闭每一条规则
- @typescript-eslint/parser:将TS转化为ESTree,这样才能被eslint检测到
- 配置 .eslintrc
- Parser: 指定ESLint使用的语法分析器:如Esprima、Babel-ESLint、@typescript-eslint/parser 默认Esprima
- parserOptions: {
ecmaVersion: 6 // es版本
sourceType: 'module', // 设置为 "script" (默认) 或 "module"(ES6)。
ecmaFeatures: { // 这是个对象,表示你想使用的额外的语言特性: jsx: true // 启用 JSX }
},
- extends:继承的规则,可以在rules进行覆盖
- Plugins: 使用第三方插件
- rules:规则("off"或0 -关闭规则;"warn" 或1 - 开启规则, 使用警告 程序不会退出;"error"或2 - 开启规则, 使用错误 程序退出)
Prettier
- 统一团队的编码风格,保证代码的可读性,可设置保存自动格式化
- 安装依赖
- prettier:按照配置格式化代码
- eslint-config-prettier: 禁用任何可能干扰现有 prettier 规则的 linting 规则
- Eslint-plugin-prettier: 作为ESLint的一部分运行Prettier分析
-
配置 .eslintrc.js
{
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 80,
"semi": true,
"tabWidth": 4,
"useTabs": false }
工具:TypeScript Playground with React
可以在线调试React+TypeScript ,注意:只能调试类型,不能运行代码
VSCode 编辑器
在 workspace settings 中配置检测文件范围,确保 React 项目中 .ts
和 .tsx
文件有自动修复功能。
{
"eslint.validate": ["typescript", "typescriptreact"]
}
配置 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 // 为装饰器提供元数据的支持
}
}
React 组件声明
-
类组件,使用React.Component<P,S> 和React.PureComponent<P,S,SS>进行定义
- P是Props类型,S为State类型,SS为 Snapshot 返回的值
- 类组件除了需要约束Props 参数外,还需要给State 进行定义。如果有可选参数,需要定义默认值,类组件中使用 static 关键字。
- React 是单向数据流,Props 是不允许在子组件内被修改的,那我们需要手动为每个属性都加上Readyonly?
- ts playground中试试
-
React 函数组件,使用React.FunctionComponent定义函数组件,一种是直接给Props写定义
- React.FC在定义内部已经定义Children 的类型和函数返回值,可以直接用Children
- Props则需要自己定义Children类型
- ts playground中试试
-
无状态组件:在 React 的声明文件中 已经定义了一个 SFC 类型
// 无状态组件 interface IProps { onClick(event: MouseEvent)=> void } const Button: React.SFC = ({ onClick, children }) => { return
{children}} -
JSX.Element vs ReactNode vs ReactElement
ReactElement 是具有类型和属性的对象,通过执行 React.createElement 或是转译 JSX 获得
ReactNode是多种类型的集合,是ReactElement,ReactFragment,字符串,ReactNodes的数字或数组,或者为Null,未定义或 Bool 值
类组件类型定义:通过 render() 返回 ReactNode,比 React 的实际值范围更宽松
函数组件类型定义:返回 JSX.Element,也比 React 的实际值范围更宽松
//React.ReactElement
const ComA: React.ReactElement<myComponent> = < MyComponent />
// 接受一个可以在 Props 起作用,并使用 JSX 渲染的组件
const ComB: React.Component< ComProps > = ComA;
// Render ComB with some props:
<ComB {... ComProps} />;
// React.ReactNode:渲染一些像 JSX 或者是 string 的内容
interface Props = {
header: React.ReactNode;
body: React.ReactNode;
};
const MyComPonent2:React.FunctionComponent<Props> = (props)=>{
const { header, body, footer } = props
return (
<>
{header}
{body}
</>
)
}
<MyComponent2 header={<h1>Header</h1>} body={<i>body</i>} />
- 实现一个通用组件 ts playground中试试
React Hook
-
useState ts playground中试试
-
useEffect
//2. useEffect: useEffect 传入的函数,它的返回值要么是一个方法(清理函数),要么就是undefined,其他情况都会报错。 //asyanc 默认返回一个promise ,导致ts报错 useEffect(async () => { const user = await getUser() setUser(user) }, []) //推荐用法 useEffect(()=>{ const getUser = async ()=>{ const user = await getUser() setUser(user) } getUser() },[])
-
useRef
- 使用 useRef 时,我们一般有两种方式去创建没有初始值的 Ref 容器。
// option 1 const ref1 = useRef(null); // option 2
const ref2 = useRef(null!);
// option 3 const ref3 = useRef<HTMLInputElement | null>(null); -
两者的区别?RefObject VS MutableRefObject
-
forwardRef
- 因为函数组件没有实例,所以函数组件无法像类组件一样可以接收 ref 属性
- ts playground中试试
-
useImperativeHandle
- 自定义暴露给父组件的值
- ts playground中试试
-
useReducer
-
自定义Hook
- 使用自定义钩子,可以提取和复用组件逻辑
- 写自定义 Hook 时,Hook 返回一个数组,则要避免类型推断
- 需要自定义返回值 or 将返回的数组断言 const
- ts playground中试试
事件处理
-
Event事件对象类型
- 所有的类型定义都有相同的格式:
React.事件名<ReactNode>
- ts playground中试试
//不在乎事件类型,可以使用React.SyntheticEvent (@types/react/index.d.ts)所有的事件都是他的子类型 //1. 表单事件 const onSumbit = (e:React.ChangeEvent)=>{...} //2. input 事件 const onChange = (e:React.ChangeEvent)=>{...} //其他事件 // 1. ClipboardEvent<T = Element> 剪贴板事件对象 // 2. DragEvent<T = Element> 拖拽事件对象 // 3. ChangeEvent<T = Element> Change 事件对象 // 4. KeyboardEvent<T = Element> 键盘事件对象 // 5. MouseEvent<T = Element> 鼠标事件对象 // 6. TouchEvent<T = Element> 触摸事件对象 // 7. WheelEvent<T = Element> 滚轮事件对象 // 8. AnimationEvent<T = Element> 动画事件对象 // 9. TransitionEvent<T = Element> 过渡事件对象
- 所有的类型定义都有相同的格式:
-
事件处理函数类型
当我们定义事件处理函数时有没有更方便定义其函数类型的方式呢?-------EventHandler
EventHandler 接收 E ,其代表事件处理函数中 Event 对象的类型。
interface IProps {
onClick : MouseEventHandler<HTMLDivElement>,
onChange: ChangeEventHandler<HTMLDivElement>
}
Promise类型
在代码中,我们会遇到Async函数,调用的时候返回的是一个Promise对象,怎么定义呢?
Promise是一个泛型类型,T泛型变量用于确定使用 then 方法时接收的第一个回调函数(onfulfilled)的参数类型
interface IResponse<T> {
message: string,
result: T,
success: boolean,
}
async function getResponse (): Promise<IResponse<number[]>> {
return {
message: '获取成功',
result: [1, 2, 3],
success: true,
}
}
getResponse()
.then(response => {
console.log(response.result)
})
实用技巧
-
Interface or type
- 扩展:interface基于extends,type 基于交叉类型 &
- type 可以声明基本类型别名,联合类型,元组等类型,配合 typeof 获取实例类型。
- interface 能够声明合并
- 能用interface实现优先interface
-
类型提取(index type、mapped type、keyof)
interface UserInfo{ id:number name:string status: 1|2|3|4 } // index type type UserStatus = { id: UserInfo['id']
status:UserInfo['status'] } // mapped type type UserStatus = { [K in 'id'|'status']:Userinfo[K] } // keyof function getStatus<T extends { [key:string]:any },K extends keyof T>(obj:T,key:K):T[K]{ return obj[key] } const status = getStatus(UserInfo,'status') -
巧用 typeof 快速定义接口类型
const INIT_OPTIONS = { id:101, name:'banggan', age:26, tel:1809999999, }; interface Options { id: number name: string age:number tel:number } type Options = typeof INIT_OPTIONS // Ts 中的 typeof 可以用来获取一个真实的变量、对象的类型,也可以用来获取函数的类型
-
工具泛型 ts playground中试试
const info = { name:'banggan', age:26, sex:'man' location:'beijing' tel:88888888 } // sex 需要数字映射----枚举 enum SEX_MAP{ 'man', 'woman' } const info1 = { sex:SEX_MAP.man } // 字段重构为数字映射,之前的字符串则表明处理----keyof valueof // keyof返回一个类型里面所有的key组成的联合类型 // ts没有valueof关键字,T[keyof T]就是对标keyof的valueof的效果 interface SEX_MAP1 { man:0, woman:1 } interface Info { name: string age: number sex: keyof SEX_MAP1 | SEX_MAP1[keyof SEX_MAP1] } const info2: Info = { name:'sb', age:1, sex: 'man' } const oldInfo: Info={ name:'1', age: 1, sex: 0 } // info全部为string Record<T,U> T 传入的jey U表示对于key的value的类型 const s: Record<keyof Info,string> = { name:'123', age:'111', sex:'man' }
type MyRcord<T extends keyof any,U> = { [k in T]:U } // 全部变为可选 type MyPartial = { [P in keyof T]?: T[P] } // 全部为必填 type MyRequired ={ [P in keyof T]-?:T[P] } // 全部变为可读 type MyReadonly = { readonly [P in keyof T] :T[P] } // 如果只想取部分属性 进行required等操作----Pick // Pick是指从T里面挑几个key,如Pick<type1, ‘key1’ | ‘key2’>。先把你所希望变成可选的key选出来,再交叉类型补上剩下的 type Info1 = Partial<Pick<Info,'name' | 'sex'>> & {age:number} const info3: Info1 = { name:'1111', age:1 } //删除某个属性 // Omit和pick相反,选出一个类型里面除了这些给定key的剩下的key。如Omit<type1, ‘key1’ | ‘key2’>,表示选取type1中除了key1和key2的其他key。 type Info2 = Partial<Pick<Info,'name'|'sex'>> & Omit<Info,'name'|'sex'> //pick 的实现----in type MyPick<T, K extends keyof T> = { [P in K]:T[P] } //限制的是T里面存在的key--- 约束范围放大---condition type type SuperPick<T,K extends keyof any> = { [P in K extends keyof T ? K:never]:T[P] } // condition type表示条件类型,类似三元表达式,前面的条件部分语句需要使用extends,条件就是 A extends B type isNumber = T extends number ?T:never type test1 = [isNumber<1>,isNumber,isNumber<'1'>] // omit的实现 pick出来K里面的key集合,再pick剩下的key type MyExclude<T,U> = T extends U ? never:T type MyOmit<T,K extends keyof any> = Pick<T,MyExclude<keyof T,K>> // infer ---- 表示在condition type 的条件语句中待推断的类型变量,例如returntype就是靠infer实现的 type MyReturnType = T extends (...args:any[])=> infer P ? P:any // 解promise 取数组类型的item都可以这样操作
//一个对象两种key组合形式---联合类型解决 // 像document.queryselector这种,确实不知道返回值 // is类型断言 function isDiv(ele:Element |null):ele is HTMLDivElement{ return ele && ele.nodeName === "DIV" } function isCanvas(ele:Element |null):ele is HTMLCanvasElement{ return ele && ele.nodeName === "CANVAS" } function commonQuery(selector:string){ const ele = document.querySelector(selector) if(isDiv(ele)){ console.log(ele.innerHTML) }else if (isCanvas(ele)){ console.log(ele.getContext) } } // window window下的属性 interface Window { a:number }
作者:肖芳