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 处理转译。你仍然需要指定 jsxFactory
和 jsxFragmentFactory
才能获得正确的类型。
¥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
useState
、useEffect
和 useContext
都具有通用类型,因此你无需额外注释。下面是使用 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
只能传递给 HTMLInputElement
。useRef
通常用 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 元素添加其他属性以与特定库一起使用。为此,你需要分别扩展 IntrinsicElements
或 HTMLAttributes
接口,以便 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 {}