TypeScript

Preact 提供了 TypeScript 类型定义,由库本身使用!

¥Preact ships TypeScript type definitions, which are used by the library itself!

当你在支持 TypeScript 的编辑器(如 VSCode)中使用 Preact 时,你可以在编写常规 JavaScript 时从添加的类型信息中受益。如果你想向自己的应用添加类型信息,可以使用 JSDoc 注释,或者编写 TypeScript 并转换为常规 JavaScript。本节将重点讨论后者。

¥When you use Preact in a TypeScript-aware editor (like VSCode), you can benefit from the added type information while writing regular JavaScript. If you want to add type information to your own applications, you can use JSDoc annotations, or write TypeScript and transpile to regular JavaScript. This section will focus on the latter.



TypeScript 配置

¥TypeScript configuration

TypeScript 包含一个成熟的 JSX 编译器,你可以使用它来代替 Babel。将以下配置添加到 tsconfig.json 以将 JSX 转换为与 Preact 兼容的 JavaScript:

¥TypeScript includes a full-fledged JSX compiler that you can use instead of Babel. Add the following configuration to your tsconfig.json to transpile JSX to Preact-compatible JavaScript:

// Classic Transform
{
  "compilerOptions": {
    "jsx": "react",
    "jsxFactory": "h",
    "jsxFragmentFactory": "Fragment",
    //...
  }
}
// Automatic Transform, available in TypeScript >= 4.1.1
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact",
    //...
  }
}

如果你在 Babel 工具链中使用 TypeScript,请将 jsx 设置为 preserve 并让 Babel 处理转译。你仍然需要指定 jsxFactoryjsxFragmentFactory 才能获得正确的类型。

¥If you use TypeScript within a Babel toolchain, set jsx to preserve and let Babel handle the transpilation. You still need to specify jsxFactory and jsxFragmentFactory to get the correct types.

{
  "compilerOptions": {
    "jsx": "preserve",
    "jsxFactory": "h",
    "jsxFragmentFactory": "Fragment",
    //...
  }
}

在你的 .babelrc 中:

¥In your .babelrc:

{
  presets: [
    "@babel/env",
    ["@babel/typescript", { jsxPragma: "h" }],
  ],
  plugins: [
    ["@babel/transform-react-jsx", { pragma: "h" }]
  ],
}

.jsx 文件重命名为 .tsx,以便 TypeScript 正确解析你的 JSX。

¥Rename your .jsx files to .tsx for TypeScript to correctly parse your JSX.

TypeScript preact/compat 配置

¥TypeScript preact/compat configuration

你的项目可能需要更广泛的 React 生态系统的支持。为了使你的应用编译,你可能需要禁用 node_modules 上的类型检查并添加类型的路径,如下所示。这样,当库导入 React 时,你的别名将正常工作。

¥Your project could need support for the wider React ecosystem. To make your application compile, you might need to disable type checking on your node_modules and add paths to the types like this. This way, your alias will work properly when libraries import React.

{
  "compilerOptions": {
    ...
    "skipLibCheck": true,
    "baseUrl": "./",
    "paths": {
      "react": ["./node_modules/preact/compat/"],
      "react/jsx-runtime": ["./node_modules/preact/jsx-runtime"],
      "react-dom": ["./node_modules/preact/compat/"],
      "react-dom/*": ["./node_modules/preact/compat/*"]
    }
  }
}

类型组件

¥Typing components

在 Preact 中输入组件的方式有多种。类组件具有泛型类型变量以确保类型安全。只要函数返回 JSX,TypeScript 就会将其视为功能组件。有多种解决方案来定义功能组件的 props。

¥There are different ways to type components in Preact. Class components have generic type variables to ensure type safety. TypeScript sees a function as functional component as long as it returns JSX. There are multiple solutions to define props for functional components.

函数组件

¥Function components

输入常规函数组件就像向函数参数添加类型信息一样简单。

¥Typing regular function components is as easy as adding type information to the function arguments.

interface MyComponentProps {
  name: string;
  age: number;
};

function MyComponent({ name, age }: MyComponentProps) {
  return (
    <div>
      My name is {name}, I am {age.toString()} years old.
    </div>
  );
}

你可以通过在函数签名中设置默认值来设置默认属性。

¥You can set default props by setting a default value in the function signature.

interface GreetingProps {
  name?: string; // name is optional!
}

function Greeting({ name = "User" }: GreetingProps) {
  // name is at least "User"
  return <div>Hello {name}!</div>
}

Preact 还提供了 FunctionComponent 类型来注释匿名函数。FunctionComponent 还为 children 添加了类型:

¥Preact also ships a FunctionComponent type to annotate anonymous functions. FunctionComponent also adds a type for children:

import { h, FunctionComponent } from "preact";

const Card: FunctionComponent<{ title: string }> = ({ title, children }) => {
  return (
    <div class="card">
      <h1>{title}</h1>
      {children}
    </div>
  );
};

children 属于 ComponentChildren 类型。你可以使用此类型自行指定子项:

¥children is of type ComponentChildren. You can specify children on your own using this type:

import { h, ComponentChildren } from "preact";

interface ChildrenProps {
  title: string;
  children: ComponentChildren;
}

function Card({ title, children }: ChildrenProps) {
  return (
    <div class="card">
      <h1>{title}</h1>
      {children}
    </div>
  );
};

类组件

¥Class components

Preact 的 Component 类被定型为具有两个泛型类型变量的泛型:属性和状态。这两种类型都默认为空对象,你可以根据需要指定它们。

¥Preact's Component class is typed as a generic with two generic type variables: Props and State. Both types default to the empty object, and you can specify them according to your needs.

// Types for props
interface ExpandableProps {
  title: string;
};

// Types for state
interface ExpandableState {
  toggled: boolean;
};


// Bind generics to ExpandableProps and ExpandableState
class Expandable extends Component<ExpandableProps, ExpandableState> {
  constructor(props: ExpandableProps) {
    super(props);
    // this.state is an object with a boolean field `toggle`
    // due to ExpandableState
    this.state = {
      toggled: false
    };
  }
  // `this.props.title` is string due to ExpandableProps
  render() {
    return (
      <div class="expandable">
        <h2>
          {this.props.title}{" "}
          <button
            onClick={() => this.setState({ toggled: !this.state.toggled })}
          >
            Toggle
          </button>
        </h2>
        <div hidden={this.state.toggled}>{this.props.children}</div>
      </div>
    );
  }
}

类组件默认包含子组件,类型为 ComponentChildren

¥Class components include children by default, typed as ComponentChildren.

类型事件

¥Typing events

Preact 触发常规 DOM 事件。只要你的 TypeScript 项目包含 dom 库(在 tsconfig.json 中设置),你就可以访问当前配置中可用的所有事件类型。

¥Preact emits regular DOM events. As long as your TypeScript project includes the dom library (set it in tsconfig.json), you have access to all event types that are available in your current configuration.

export class Button extends Component {
  handleClick(event: MouseEvent) {
    event.preventDefault();
    if (event.target instanceof HTMLElement) {
      alert(event.target.tagName); // Alerts BUTTON
    }
  }

  render() {
    return <button onClick={this.handleClick}>{this.props.children}</button>;
  }
}

你可以通过将 this 的类型注释添加到函数签名作为第一个参数来限制事件处理程序。该参数将在转译后被删除。

¥You can restrict event handlers by adding a type annotation for this to the function signature as the first argument. This argument will be erased after transpilation.

export class Button extends Component {
  // Adding the this argument restricts binding
  handleClick(this: HTMLButtonElement, event: MouseEvent) {
    event.preventDefault();
    if (event.target instanceof HTMLElement) {
      console.log(event.target.localName); // "button"
    }
  }

  render() {
    return (
      <button onClick={this.handleClick}>{this.props.children}</button>
    );
  }
}

类型参考文献

¥Typing references

createRef 函数也是通用的,允许你将引用绑定到元素类型。在此示例中,我们确保引用只能绑定到 HTMLAnchorElement。将 ref 与任何其他元素一起使用会让 TypeScript 抛出错误:

¥The createRef function is also generic, and lets you bind references to element types. In this example, we ensure that the reference can only be bound to HTMLAnchorElement. Using ref with any other element lets TypeScript thrown an error:

import { h, Component, createRef } from "preact";

class Foo extends Component {
  ref = createRef<HTMLAnchorElement>();

  componentDidMount() {
    // current is of type HTMLAnchorElement
    console.log(this.ref.current);
  }

  render() {
    return <div ref={this.ref}>Foo</div>;
    //          ~~~
    //       💥 Error! Ref only can be used for HTMLAnchorElement
  }
}

如果你想确保 ref 到的元素是可以是例如的输入元素,这会很有帮助。 专注。

¥This helps a lot if you want to make sure that the elements you ref to are input elements that can be e.g. focussed.

类型上下文

¥Typing context

createContext 尝试从你传递给的初始值中尽可能多地推断:

¥createContext tries to infer as much as possible from the intial values you pass to:

import { h, createContext } from "preact";

const AppContext = createContext({
  authenticated: true,
  lang: "en",
  theme: "dark"
});
// AppContext is of type preact.Context<{
//   authenticated: boolean;
//   lang: string;
//   theme: string;
// }>

它还要求你传入你在初始值中定义的所有属性:

¥It also requires you to pass in all the properties you defined in the initial value:

function App() {
  // This one errors 💥 as we haven't defined theme
  return (
    <AppContext.Provider
      value={{
//    ~~~~~ 
// 💥 Error: theme not defined
        lang: "de",
        authenticated: true
      }}
    >
    {}
      <ComponentThatUsesAppContext />
    </AppContext.Provider>
  );
}

如果你不想指定所有属性,可以将默认值与替代值合并:

¥If you don't want to specify all properties, you can either merge default values with overrides:

const AppContext = createContext(appContextDefault);

function App() {
  return (
    <AppContext.Provider
      value={{
        lang: "de",
        ...appContextDefault
      }}
    >
      <ComponentThatUsesAppContext />
    </AppContext.Provider>
  );
}

或者,你可以不使用默认值,并使用绑定泛型类型变量将上下文绑定到特定类型:

¥Or you work without default values and use bind the generic type variable to bind context to a certain type:

interface AppContextValues {
  authenticated: boolean;
  lang: string;
  theme: string;
}

const AppContext = createContext<Partial<AppContextValues>>({});

function App() {
  return (
    <AppContext.Provider
      value={{
        lang: "de"
      }}
    >
      <ComponentThatUsesAppContext />
    </AppContext.Provider>
  );

所有值都是可选的,因此在使用它们时必须进行 null 检查。

¥All values become optional, so you have to do null checks when using them.

类型钩子

¥Typing hooks

大多数钩子不需要任何特殊的类型信息,但可以根据使用情况推断类型。

¥Most hooks don't need any special typing information, but can infer types from usage.

useState、useEffect、useContext

useStateuseEffectuseContext 都具有通用类型,因此你无需额外注释。下面是使用 useState 的最小组件,所有类型都是从函数签名的默认值推断出来的。

¥useState, useEffect and useContext all feature generic types so you don't need to annotate extra. Below is a minimal component that uses useState, with all types infered from the function signature's default values.

const Counter = ({ initial = 0 }) => {
  // since initial is a number (default value!), clicks is a number
  // setClicks is a function that accepts 
  // - a number 
  // - a function returning a number
  const [clicks, setClicks] = useState(initial);
  return (
    <>
      <p>Clicks: {clicks}</p>
      <button onClick={() => setClicks(clicks + 1)}>+</button>
      <button onClick={() => setClicks(clicks - 1)}>-</button>
    </>
  );
};

useEffect 会进行额外的检查,因此你只返回清理函数。

¥useEffect does extra checks so you only return cleanup functions.

useEffect(() => {
  const handler = () => {
    document.title = window.innerWidth.toString();
  };
  window.addEventListener("resize", handler);

  // ✅  if you return something from the effect callback
  // it HAS to be a function without arguments
  return () => {
    window.removeEventListener("resize", handler);
  };
});

useContext 从你传递给 createContext 的默认对象中获取类型信息。

¥useContext gets the type information from the default object you pass into createContext.

const LanguageContext = createContext({ lang: 'en' });

const Display = () => {
  // lang will be of type string
  const { lang } = useContext(LanguageContext);
  return <>
    <p>Your selected language: {lang}</p>
  </>
}

useRef

就像 createRef 一样,useRef 受益于将泛型类型变量绑定到 HTMLElement 的子类型。在下面的示例中,我们确保 inputRef 只能传递给 HTMLInputElementuseRef 通常用 null 初始化,在启用 strictNullChecks 标志的情况下,我们需要检查 inputRef 是否实际上可用。

¥Just like createRef, useRef benefits from binding a generic type variable to a subtype of HTMLElement. In the example below, we make sure that inputRef only can be passed to HTMLInputElement. useRef is usually initialized with null, with the strictNullChecks flag enabled, we need to check if inputRef is actually available.

import { h } from "preact";
import { useRef } from "preact/hooks";

function TextInputWithFocusButton() {
  // initialise with null, but tell TypeScript we are looking for an HTMLInputElement
  const inputRef = useRef<HTMLInputElement>(null);
  const focusElement = () => {
    // strict null checks need us to check if inputEl and current exist.
    // but once current exists, it is of type HTMLInputElement, thus it
    // has the method focus! ✅
    if(inputRef && inputRef.current) {
      inputRef.current.focus();
    } 
  };
  return (
    <>
      { /* in addition, inputEl only can be used with input elements */ }
      <input ref={inputRef} type="text" />
      <button onClick={focusElement}>Focus the input</button>
    </>
  );
}

useReducer

对于 useReducer 钩子,TypeScript 尝试从化简函数推断尽可能多的类型。例如,参见计数器的 reducer。

¥For the useReducer hook, TypeScript tries to infer as many types as possible from the reducer function. See for example a reducer for a counter.

// The state type for the reducer function
interface StateType {
  count: number;
}

// An action type, where the `type` can be either
// "reset", "decrement", "increment"
interface ActionType {
  type: "reset" | "decrement" | "increment";
}

// The initial state. No need to annotate
const initialState = { count: 0 };

function reducer(state: StateType, action: ActionType) {
  switch (action.type) {
    // TypeScript makes sure we handle all possible
    // action types, and gives auto complete for type
    // strings
    case "reset":
      return initialState;
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    default:
      return state;
  }
}

一旦我们在 useReducer 中使用 reducer 函数,我们就会推断出几种类型并对传递的参数进行类型检查。

¥Once we use the reducer function in useReducer, we infer several types and do type checks for passed arguments.

function Counter({ initialCount = 0 }) {
  // TypeScript makes sure reducer has maximum two arguments, and that
  // the initial state is of type Statetype.
  // Furthermore:
  // - state is of type StateType
  // - dispatch is a function to dispatch ActionType
  const [state, dispatch] = useReducer(reducer, { count: initialCount });

  return (
    <>
      Count: {state.count}
      {/* TypeScript ensures that the dispatched actions are of ActionType */}
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </>
  );
}

唯一需要的注释是在 reducer 函数本身中。useReducer 类型还确保 reducer 函数的返回值是 StateType 类型。

¥The only annotation needed is in the reducer function itself. The useReducer types also ensure that the return value of the reducer function is of type StateType.

扩展内置 JSX 类型

¥Extending built-in JSX types

你可能有想要在 JSX 中使用的 自定义元素,或者你可能希望向所有 HTML 元素添加其他属性以与特定库一起使用。为此,你需要分别扩展 IntrinsicElementsHTMLAttributes 接口,以便 TypeScript 能够感知并提供正确的类型信息。

¥You may have custom elements that you'd like to use in JSX, or you may wish to add additional attributes to all HTML elements to work with a particular library. To do this, you will need to extend the IntrinsicElements or HTMLAttributes interfaces, respectively, so that TypeScript is aware and can provide correct type information.

扩展 IntrinsicElements

¥Extending IntrinsicElements

function MyComponent() {
  return <loading-bar showing={true}></loading-bar>;
  //      ~~~~~~~~~~~
  //   💥 Error! Property 'loading-bar' does not exist on type 'JSX.IntrinsicElements'.
}
// global.d.ts

declare global {
  namespace preact.JSX {
    interface IntrinsicElements {
      'loading-bar': { showing: boolean };
    }
  }
}

// This empty export is important! It tells TS to treat this as a module
export {}

扩展 HTMLAttributes

¥Extending HTMLAttributes

function MyComponent() {
  return <div custom="foo"></div>;
  //          ~~~~~~
  //       💥 Error! Type '{ custom: string; }' is not assignable to type 'DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'.
  //                   Property 'custom' does not exist on type 'DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'.
}
// global.d.ts

declare global {
  namespace preact.JSX {
    interface HTMLAttributes {
      custom?: string | undefined;
    }
  }
}

// This empty export is important! It tells TS to treat this as a module
export {}
Preact 中文网 - 粤ICP备13048890号