乐趣区

关于前端:你要的reactts最佳实践指南

本文依据日常开发实际,参考优良文章、文档,来说说 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 Array<string>
    id: string | number; // 联结类型
}

联结类型

个别的联结类型,没什么好说的,这里提一下十分有用,但老手常常忘记的写法 —— 字符字面量联结。

  • 例如:自定义 ajax 时,个别 method 就那么具体的几种:getpostput 等。
    大家都晓得须要传入一个 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 的汇合作为返回对象的属性,且值类型为 T
type 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;

益处:

  1. 当你书写 home 值时,键入 h 罕用的编辑器有智能补全提醒;
  2. home 拼写错误成 hoem,会有谬误提醒,往往这类谬误很荫蔽;
  3. 收窄接管的边界。

函数类型

  • 函数类型不倡议间接给 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 类型定义时,而你只想关注本人的下层定义。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, 但没思考 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(or 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] = useState<AppProps | null>(null);
    // const [data] = useState<AppProps | undefined>();
    return <div>{data && data.message}</div>;
};
  • 更优雅,链式判断
// data && data.message
data?.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 不匹配。
// ❌ 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 个别用于两种场景

  1. 援用 DOM 元素;
  2. 不想作为其余 hooks 的依赖项,因为 ref 的值援用是不会变的,变的只是 ref.current
  3. 应用 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 来准确辨识、收窄确定的 typepayload 类型。
个别也须要定义 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'})}>-</button>
            <button onClick={() => dispatch({ type: 'increment', payload: 5})}>+</button>
        </>
    );
}

useContext

个别 useContextuseReducer 联合应用,来治理全局的数据流。

  • 例子
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>
    );
};

// 生产 context
function 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.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];
退出移动版