本文依据日常开发实际,参考优良文章、文档,来说说 TypeScript
是如何较优雅的融入 React
我的项目的。
舒适提醒:日常开发中已全面拥抱函数式组件和 React Hooks
,class
类组件的写法这里不提及。
前沿
- 以前有 JSX 语法,必须引入 React。React 17.0+ 不须要强制申明 React 了。
import React, { useState } from 'react';// 当前将被代替成import { useState } from 'react';import * as React from 'react';
根底介绍
根本类型
- 根底类型就没什么好说的了,以下都是比拟罕用的,个别比拟好了解,也没什么问题。
type BasicTypes = { message: string; count: number; disabled: boolean; names: string[]; // or Array<string> id: string | number; // 联结类型}
联结类型
个别的联结类型,没什么好说的,这里提一下十分有用,但老手常常忘记的写法 —— 字符字面量联结。
- 例如:自定义
ajax
时,个别method
就那么具体的几种:get
、post
、put
等。
大家都晓得须要传入一个string
型,你可能会这么写:
type UnionsTypes = { method: string; // ❌ bad,能够传入任意字符串};
- 应用字符字面量联结类型,第一、能够智能提醒你可传入的字符常量;第二、避免拼写错误。前面会有更多的例子。
type UnionsTypes = { method: 'get' | 'post'; // ✅ good 只容许 'get'、'post' 字面量};
对象类型
- 个别你晓得确切的属性类型,这没什么好说的。
type ObjectTypes = { obj3: { id: string; title: string; }; objArr: { id: string; title: string; }[]; // 对象数组,or Array<{ id: string, title: string }>};
- 但有时你只晓得是个对象,而不确定具体有哪些属性时,你可能会这么用:
type ObjectTypes = { obj: object; // ❌ bad,不举荐 obj2: {}; // ❌ bad 简直相似 object};
- 个别编译器会提醒你,不要这么应用,举荐应用
Record
。
type ObjectTypes = { objBetter: Record<string, unknown>; // ✅ better,代替 obj: object // 对于 obj2: {}; 有三种状况: obj2Better1: Record<string, unknown>; // ✅ better 同上 obj2Better2: unknown; // ✅ any value obj2Better3: Record<string, never>; // ✅ 空对象 /** Record 更多用法 */ dict1: { [key: string]: MyTypeHere; }; dict2: Record<string, MyTypeHere>; // 等价于 dict1};
Record
有什么益处呢,先看看实现:
// 意思就是,泛型 K 的汇合作为返回对象的属性,且值类型为 Ttype Record<K extends keyof any, T> = { [P in K]: T;};
- 官网的一个例子
interface PageInfo { title: string;}type Page = 'home' | 'about' | 'contact';const nav: Record<Page, PageInfo> = { about: { title: 'about' }, contact: { title: 'contact' }, // TS2322: Type '{ about: { title: string; }; contact: { title: string; }; hoem: { title: string; }; }' // is not assignable to type 'Record<Page, PageInfo>'. ... hoem: { title: 'home' },};nav.about;
益处:
- 当你书写
home
值时,键入h
罕用的编辑器有智能补全提醒; home
拼写错误成hoem
,会有谬误提醒,往往这类谬误很荫蔽;- 收窄接管的边界。
相干React实战视频解说:进入学习
函数类型
- 函数类型不倡议间接给
Function
类型,有明确的参数类型、个数与返回值类型最佳。
type FunctionTypes = { onSomething: Function; // ❌ bad,不举荐。任何可调用的函数 onClick: () => void; // ✅ better ,明确无参数无返回值的函数 onChange: (id: number) => void; // ✅ better ,明确参数无返回值的函数 onClick(event: React.MouseEvent<HTMLButtonElement>): void; // ✅ better};
可选属性
- React props 可选的状况下,比拟罕用。
type OptionalTypes = { optional?: OptionalType; // 可选属性};
- 例子:封装一个第三方组件,对方可能并没有裸露一个 props 类型定义时,而你只想关注本人的下层定义。
name
,age
是你新增的属性,age
可选,other
为第三方的属性集。
type AppProps = { name: string; age?: number; [propName: string]: any;};const YourComponent = ({ name, age, ...other }: AppProps) => ( <div> {`Hello, my name is ${name}, ${age || 'unknown'}`} <Other {...other} /> </div>);
React Prop 类型
- 如果你有配置
Eslint
等一些代码查看时,个别函数组件须要你定义返回的类型,或传入一些React
相干的类型属性。
这时理解一些React
自定义暴露出的类型就很有必要了。例如罕用的React.ReactNode
。
export declare interface AppProps { children1: JSX.Element; // ❌ bad, 没有思考数组类型 children2: JSX.Element | JSX.Element[]; // ❌ 没思考字符类型 children3: React.ReactChildren; // ❌ 名字唬人,工具类型,慎用 children4: React.ReactChild[]; // better, 但没思考 null children: React.ReactNode; // ✅ best, 最佳接管所有 children 类型 functionChildren: (name: string) => React.ReactNode; // ✅ 返回 React 节点 style?: React.CSSProperties; // React style onChange?: React.FormEventHandler<HTMLInputElement>; // 表单事件! 泛型参数即 `event.target` 的类型}
更多参考资料
函数式组件
相熟了根底的 TypeScript
应用 与 React
内置的一些类型后,咱们该开始着手编写组件了。
- 申明纯函数的最佳实际
type AppProps = { message: string }; /* 也可用 interface */const App = ({ message }: AppProps) => <div>{message}</div>; // 无大括号的箭头函数,利用 TS 推断。
- 须要隐式
children
?能够试试React.FC
。
type AppProps = { title: string };const App: React.FC<AppProps> = ({ children, title }) => <div title={title}>{children}</div>;
- 争议
React.FC
(orFunctionComponent
)是显式返回的类型,而"一般函数"版本则是隐式的(有时还须要额定的申明)。React.FC
对于动态属性如displayName
,propTypes
,defaultProps
提供了主动补充和类型查看。React.FC
提供了默认的children
属性的大而全的定义申明,可能并不是你须要的确定的小范畴类型。- 2和3都会导致一些问题。有人不举荐应用。
目前 React.FC
在我的项目中应用较多。因为能够偷懒,还没碰到极其状况。
Hooks
我的项目基本上都是应用函数式组件和 React Hooks
。
接下来介绍罕用的用 TS 编写 Hooks 的办法。
useState
- 给定初始化值状况下能够间接应用
import { useState } from 'react';// ...const [val, toggle] = useState(false);// val 被推断为 boolean 类型// toggle 只能解决 boolean 类型
- 没有初始值(undefined)或初始 null
type AppProps = { message: string };const App = () => { const [data] = useState<AppProps | null>(null); // const [data] = useState<AppProps | undefined>(); return <div>{data && data.message}</div>;};
- 更优雅,链式判断
// data && data.messagedata?.message
useEffect
- 应用
useEffect
时传入的函数简写要小心,它接管一个无返回值函数或一个革除函数。
function DelayedEffect(props: { timerMs: number }) { const { timerMs } = props; useEffect( () => setTimeout(() => { /* do stuff */ }, timerMs), [timerMs] ); // ❌ bad example! setTimeout 会返回一个记录定时器的 number 类型 // 因为简写,箭头函数的主体没有用大括号括起来。 return null;}
- 看看
useEffect
接管的第一个参数的类型定义。
// 1. 是一个函数// 2. 无参数// 3. 无返回值 或 返回一个清理函数,该函数类型无参数、无返回值 。type EffectCallback = () => (void | (() => void | undefined));
- 理解了定义后,只需注意加层大括号。
function DelayedEffect(props: { timerMs: number }) { const { timerMs } = props; useEffect(() => { const timer = setTimeout(() => { /* do stuff */ }, timerMs); // 可选 return () => clearTimeout(timer); }, [timerMs]); // ✅ 确保函数返回 void 或一个返回 void|undefined 的清理函数 return null;}
- 同理,async 解决异步申请,相似传入一个
() => Promise<void>
与EffectCallback
不匹配。
// ❌ baduseEffect(async () => { const { data } = await ajax(params); // todo}, [params]);
- 异步申请,解决形式:
// ✅ betteruseEffect(() => { (async () => { const { data } = await ajax(params); // todo })();}, [params]);// 或者 then 也是能够的useEffect(() => { ajax(params).then(({ data }) => { // todo });}, [params]);
useRef
useRef
个别用于两种场景
- 援用
DOM
元素; - 不想作为其余
hooks
的依赖项,因为ref
的值援用是不会变的,变的只是ref.current
。 - 应用
useRef
,可能会有两种形式。
const ref1 = useRef<HTMLElement>(null!);const ref2 = useRef<HTMLElement | null>(null);
- 非 null 断言
null!
。断言之后的表达式非 null、undefined
function MyComponent() { const ref1 = useRef<HTMLElement>(null!); useEffect(() => { doSomethingWith(ref1.current); // 跳过 TS null 查看。e.g. ref1 && ref1.current }); return <div ref={ref1}> etc </div>;}
- 不倡议应用
!
,存在隐患,Eslint 默认禁掉。
function TextInputWithFocusButton() { // 初始化为 null, 但告知 TS 是心愿 HTMLInputElement 类型 // inputEl 只能用于 input elements const inputEl = React.useRef<HTMLInputElement>(null); const onButtonClick = () => { // TS 会查看 inputEl 类型,初始化 null 是没有 current 上是没有 focus 属性的 // 你须要自定义判断! if (inputEl && inputEl.current) { inputEl.current.focus(); } // ✅ best inputEl.current?.focus(); }; return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> );}
useReducer
应用 useReducer
时,多多利用 Discriminated Unions 来准确辨识、收窄确定的 type
的 payload
类型。
个别也须要定义 reducer
的返回类型,不然 TS 会主动推导。
- 又是一个联结类型收窄和防止拼写错误的精妙例子。
const initialState = { count: 0 };// ❌ bad,可能传入未定义的 type 类型,或码错单词,而且还须要针对不同的 type 来兼容 payload// type ACTIONTYPE = { type: string; payload?: number | string };// ✅ goodtype ACTIONTYPE = | { type: 'increment'; payload: number } | { type: 'decrement'; payload: string } | { type: 'initial' };function reducer(state: typeof initialState, action: ACTIONTYPE) { switch (action.type) { case 'increment': return { count: state.count + action.payload }; case 'decrement': return { count: state.count - Number(action.payload) }; case 'initial': return { count: initialState.count }; default: throw new Error(); }}function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({ type: 'decrement', payload: '5' })}>-</button> <button onClick={() => dispatch({ type: 'increment', payload: 5 })}>+</button> </> );}
useContext
个别 useContext
和 useReducer
联合应用,来治理全局的数据流。
- 例子
interface AppContextInterface { state: typeof initialState; dispatch: React.Dispatch<ACTIONTYPE>;}const AppCtx = React.createContext<AppContextInterface>({ state: initialState, dispatch: (action) => action,});const App = (): React.ReactNode => { const [state, dispatch] = useReducer(reducer, initialState); return ( <AppCtx.Provider value={{ state, dispatch }}> <Counter /> </AppCtx.Provider> );};// 生产 contextfunction Counter() { const { state, dispatch } = React.useContext(AppCtx); return ( <> Count: {state.count} <button onClick={() => dispatch({ type: 'decrement', payload: '5' })}>-</button> <button onClick={() => dispatch({ type: 'increment', payload: 5 })}>+</button> </> );}
自定义 Hooks
Hooks
的美好之处不只有减小代码行的效用,重点在于可能做到逻辑与 UI 拆散。做纯正的逻辑层复用。
- 例子:当你自定义 Hooks 时,返回的数组中的元素是确定的类型,而不是联结类型。能够应用 const-assertions 。
export function useLoading() { const [isLoading, setState] = React.useState(false); const load = (aPromise: Promise<any>) => { setState(true); return aPromise.finally(() => setState(false)); }; return [isLoading, load] as const; // 推断出 [boolean, typeof load],而不是联结类型 (boolean | typeof load)[]}
- 也能够断言成
tuple type
元组类型。
export function useLoading() { const [isLoading, setState] = React.useState(false); const load = (aPromise: Promise<any>) => { setState(true); return aPromise.finally(() => setState(false)); }; return [isLoading, load] as [ boolean, (aPromise: Promise<any>) => Promise<any> ];}
- 如果对这种需要比拟多,每个都写一遍比拟麻烦,能够利用泛型定义一个辅助函数,且利用 TS 主动推断能力。
function tuplify<T extends any[]>(...elements: T) { return elements;}function useArray() { const numberValue = useRef(3).current; const functionValue = useRef(() => {}).current; return [numberValue, functionValue]; // type is (number | (() => void))[]}function useTuple() { const numberValue = useRef(3).current; const functionValue = useRef(() => { }).current; return tuplify(numberValue, functionValue); // type is [number, () => void]}
扩大
工具类型
学习 TS 好的路径是查看优良的文档和间接看 TS 或类库内置的类型。这里简略做些介绍。
- 如果你想晓得某个函数返回值的类型,你能够这么做
// foo 函数原作者并没有思考会有人须要返回值类型的需要,利用了 TS 的隐式推断。// 没有显式申明返回值类型,并 export,内部无奈复用function foo(bar: string) { return { baz: 1 };}// TS 提供了 ReturnType 工具类型,能够把推断的类型吐出type FooReturn = ReturnType<typeof foo>; // { baz: number }
- 类型能够索引返回子属性类型
function foo() { return { a: 1, b: 2, subInstArr: [ { c: 3, d: 4, }, ], };}type InstType = ReturnType<typeof foo>;type SubInstArr = InstType['subInstArr'];type SubIsntType = SubInstArr[0];const baz: SubIsntType = { c: 5, d: 6, // type checks ok!};// 也可一步到位type SubIsntType2 = ReturnType<typeof foo>['subInstArr'][0];const baz2: SubIsntType2 = { c: 5, d: 6, // type checks ok!};
同理工具类型 Parameters
也能推断出函数参数的类型。
- 简略的看看实现:关键字
infer
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
T extends (...args: any) => infer R ? R : any;
的意思是 T 可能赋值给 (...args: any) => any
的话,就返回该函数推断出的返回值类型 R
。
defaultProps
默认值问题。
type GreetProps = { age: number } & typeof defaultProps;const defaultProps = { age: 21,};const Greet = (props: GreetProps) => { // etc};Greet.defaultProps = defaultProps;
- 你可能不须要 defaultProps
type GreetProps = { age?: number };const Greet = ({ age = 21 }: GreetProps) => { // etc };
打消魔术数字/字符
自己比拟痛恨的一些代码点。
- 蹩脚的例子,看到上面这段代码不晓得你的心田,有没有羊驼奔流。
if (status === 0) { // ...} else { // ...}// ...if (status === 1) { // ...}
- 利用枚举,对立正文且语义化
// enum.tsexport enum StatusEnum { Doing, // 进行中 Success, // 胜利 Fail, // 失败}//index.tsxif (status === StatusEnum.Doing) { // ...} else { // ...}// ...if (status === StatusEnum.Success) { // ...}
- ts enum 略有争议,有的人推崇去掉 ts 代码仍旧能失常运行,显然 enum 不行。
// 对象常量export const StatusEnum = { Doing: 0, // 进行中 Success: 1, // 胜利 Fail: 2, // 失败};
- 如果字符单词自身就具备语义,你也能够用字符字面量联结类型来防止拼写错误
export declare type Position = 'left' | 'right' | 'top' | 'bottom';let position: Position;// ...// TS2367: This condition will always return 'false' since the types 'Position' and '"lfet"' have no overlap.if (position === 'lfet') { // 单词拼写错误,往往这类谬误比拟难发现 // ...}
延长:策略模式打消 if、else
if (status === StatusEnum.Doing) { return '进行中';} else if (status === StatusEnum.Success) { return '胜利';} else { return '失败';}
- 策略模式
// 对象常量export const StatusEnumText = { [StatusEnum.Doing]: '进行中', [StatusEnum.Success]: '胜利', [StatusEnum.Fail]: '失败',};// ...return StatusEnumText[status];