成都网站建设kaituozu,广东广州快速网站制作平台,网站怎么放在服务器上,网站模板的制作怎么做的本文根据日常开发实践#xff0c;参考优秀文章、文档#xff0c;来说说 TypeScript 是如何较优雅的融入 React 项目的。
温馨提示#xff1a;日常开发中已全面拥抱函数式组件和 React Hooks#xff0c;class 类组件的写法这里不提及。
前沿
以前有 JSX 语法#xff0c;…本文根据日常开发实践参考优秀文章、文档来说说 TypeScript 是如何较优雅的融入 React 项目的。
温馨提示日常开发中已全面拥抱函数式组件和 React Hooksclass 类组件的写法这里不提及。
前沿
以前有 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 Arraystringid: 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: Recordstring, unknown; // ✅ better代替 obj: object// 对于 obj2: {}; 有三种情况obj2Better1: Recordstring, unknown; // ✅ better 同上obj2Better2: unknown; // ✅ any valueobj2Better3: Recordstring, never; // ✅ 空对象/** Record 更多用法 */dict1: {[key: string]: MyTypeHere;};dict2: Recordstring, MyTypeHere; // 等价于 dict1
};Record 有什么好处呢先看看实现
// 意思就是泛型 K 的集合作为返回对象的属性且值类型为 T
type RecordK extends keyof any, T {[P in K]: T;
};官方的一个例子
interface PageInfo {title: string;
}type Page home | about | contact;const nav: RecordPage, PageInfo {about: { title: about },contact: { title: contact },// TS2322: Type { about: { title: string; }; contact: { title: string; }; hoem: { title: string; }; } // is not assignable to type RecordPage, PageInfo. ...hoem: { title: home },
};nav.about;好处
当你书写 home 值时键入 h 常用的编辑器有智能补全提示home 拼写错误成 hoem会有错误提示往往这类错误很隐蔽收窄接收的边界。
函数类型
函数类型不建议直接给 Function 类型有明确的参数类型、个数与返回值类型最佳。
type FunctionTypes {onSomething: Function; // ❌ bad不推荐。任何可调用的函数onClick: () void; // ✅ better 明确无参数无返回值的函数onChange: (id: number) void; // ✅ better 明确参数无返回值的函数onClick(event: React.MouseEventHTMLButtonElement): void; // ✅ better
};可选属性
React props 可选的情况下比较常用。
type OptionalTypes {optional?: OptionalType; // 可选属性
};例子封装一个第三方组件对方可能并没有暴露一个 props 类型定义时而你只想关注自己的上层定义。 nameage 是你新增的属性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, 但没考虑 nullchildren: React.ReactNode; // ✅ best, 最佳接收所有 children 类型functionChildren: (name: string) React.ReactNode; // ✅ 返回 React 节点style?: React.CSSProperties; // React styleonChange?: React.FormEventHandlerHTMLInputElement; // 表单事件! 泛型参数即 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.FCAppProps ({ children, title }) div title{title}{children}/div;争议
React.FCor FunctionComponent是显式返回的类型而普通函数版本则是隐式的有时还需要额外的声明。React.FC 对于静态属性如 displayNamepropTypesdefaultProps 提供了自动补充和类型检查。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] useStateAppProps | null(null);// const [data] useStateAppProps | undefined();return div{data data.message}/div;
};更优雅链式判断
// data data.message
data?.messageuseEffect
使用 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 处理异步请求类似传入一个 () Promisevoid 与 EffectCallback 不匹配。
// ❌ bad
useEffect(async () {const { data } await ajax(params);// todo
}, [params]);异步请求处理方式
// ✅ better
useEffect(() {(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 useRefHTMLElement(null!);
const ref2 useRefHTMLElement | null(null);非 null 断言 null!。断言之后的表达式非 null、undefined
function MyComponent() {const ref1 useRefHTMLElement(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 elementsconst inputEl React.useRefHTMLInputElement(null);const onButtonClick () {// TS 会检查 inputEl 类型初始化 null 是没有 current 上是没有 focus 属性的// 你需要自定义判断! if (inputEl inputEl.current) {inputEl.current.focus();}// ✅ bestinputEl.current?.focus();};return (input ref{inputEl} typetext /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 };// ✅ good
type 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 })}-/buttonbutton onClick{() dispatch({ type: increment, payload: 5 })}/button/);
}useContext
一般 useContext 和 useReducer 结合使用来管理全局的数据流。
例子
interface AppContextInterface {state: typeof initialState;dispatch: React.DispatchACTIONTYPE;
}const AppCtx React.createContextAppContextInterface({state: initialState,dispatch: (action) action,
});
const App (): React.ReactNode {const [state, dispatch] useReducer(reducer, initialState);return (AppCtx.Provider value{{ state, dispatch }}Counter //AppCtx.Provider);
};// 消费 context
function Counter() {const { state, dispatch } React.useContext(AppCtx);return (Count: {state.count} button onClick{() dispatch({ type: decrement, payload: 5 })}-/buttonbutton 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: Promiseany) {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: Promiseany) {setState(true);return aPromise.finally(() setState(false));};return [isLoading, load] as [boolean, (aPromise: Promiseany) Promiseany];
}如果对这种需求比较多每个都写一遍比较麻烦可以利用泛型定义一个辅助函数且利用 TS 自动推断能力。
function tuplifyT 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 ReturnTypetypeof foo; // { baz: number }类型可以索引返回子属性类型
function foo() {return {a: 1,b: 2,subInstArr: [{c: 3,d: 4,},],};
}type InstType ReturnTypetypeof foo;
type SubInstArr InstType[subInstArr];
type SubIsntType SubInstArr[0];const baz: SubIsntType {c: 5,d: 6, // type checks ok!
};// 也可一步到位
type SubIsntType2 ReturnTypetypeof foo[subInstArr][0];
const baz2: SubIsntType2 {c: 5,d: 6, // type checks ok!
};同理工具类型 Parameters 也能推断出函数参数的类型。
简单的看看实现关键字 infer
type ParametersT extends (...args: any) any T extends (...args: infer P) any ? P : never;
type ReturnTypeT 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.ts
export enum StatusEnum {Doing, // 进行中Success, // 成功Fail, // 失败
}//index.tsx
if (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];