TypeScript
Preact 提供 TypeScript 类型定义,这些定义由库本身使用!
在支持 TypeScript 的编辑器(如 VSCode)中使用 Preact 时,你可以在编写常规 JavaScript 时受益于添加的类型信息。如果你想向自己的应用程序添加类型信息,可以使用 JSDoc 注释,或编写 TypeScript 并编译为常规 JavaScript。本节将重点关注后者。
TypeScript 配置
TypeScript 包含一个完整的 JSX 编译器,你可以使用它来代替 Babel。将以下配置添加到你的 tsconfig.json
以将 JSX 编译为与 Preact 兼容的 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
以获取正确的类型。
{
"compilerOptions": {
"jsx": "preserve",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
//...
}
}
在你的 .babelrc
中
{
presets: [
"@babel/env",
["@babel/typescript", { jsxPragma: "h" }],
],
plugins: [
["@babel/transform-react-jsx", { pragma: "h" }]
],
}
将你的 .jsx
文件重命名为 .tsx
,以便 TypeScript 正确解析你的 JSX。
TypeScript preact/compat 配置
你的项目可能需要对更广泛的 React 生态系统提供支持。为了使你的应用程序编译,你可能需要禁用对 node_modules
的类型检查,并像这样添加类型路径。这样,当库导入 React 时,你的别名将正常工作。
{
"compilerOptions": {
...
"skipLibCheck": true,
"baseUrl": "./",
"paths": {
"react": ["./node_modules/preact/compat/"],
"react-dom": ["./node_modules/preact/compat/"]
}
}
}
输入组件
在 Preact 中输入组件有不同的方法。类组件具有泛型类型变量以确保类型安全。只要 TypeScript 返回 JSX,它就会将函数视为函数组件。有多种解决方案可以为函数组件定义属性。
函数组件
输入常规函数组件就像向函数参数添加类型信息一样简单。
interface MyComponentProps {
name: string;
age: number;
};
function MyComponent({ name, age }: MyComponentProps) {
return (
<div>
My name is {name}, I am {age.toString()} years old.
</div>
);
}
你可以通过在函数签名中设置默认值来设置默认属性。
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
添加了一个类型
import { h, FunctionComponent } from "preact";
const Card: FunctionComponent<{ title: string }> = ({ title, children }) => {
return (
<div class="card">
<h1>{title}</h1>
{children}
</div>
);
};
children
的类型是 ComponentChildren
。你可以使用此类型自己指定子级
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>
);
};
类组件
Preact 的 Component
类被键入为一个泛型,具有两个泛型类型变量:Props 和 State。两种类型都默认为空对象,你可以根据需要指定它们。
// 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
。
键入事件
Preact 发射常规 DOM 事件。只要你的 TypeScript 项目包含 dom
库(在 tsconfig.json
中设置),你就可以访问当前配置中可用的所有事件类型。
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
的类型注释作为第一个参数添加到函数签名中来限制事件处理程序。此参数将在转换后被删除。
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>
);
}
}
键入引用
createRef
函数也是通用的,它允许你将引用绑定到元素类型。在此示例中,我们确保引用只能绑定到 HTMLAnchorElement
。对任何其他元素使用 ref
会让 TypeScript 抛出错误
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
的元素是可以聚焦的输入元素,这很有帮助。
键入上下文
createContext
尝试从你传递给它的初始值中推断尽可能多的信息
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;
// }>
它还要求你传递你在初始值中定义的所有属性
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>
);
}
如果你不想指定所有属性,你可以将默认值与覆盖值合并
const AppContext = createContext(appContextDefault);
function App() {
return (
<AppContext.Provider
value={{
lang: "de",
...appContextDefault
}}
>
<ComponentThatUsesAppContext />
</AppContext.Provider>
);
}
或者,你可以在没有默认值的情况下工作,并使用绑定泛型类型变量将上下文绑定到特定类型
interface AppContextValues {
authenticated: boolean;
lang: string;
theme: string;
}
const AppContext = createContext<Partial<AppContextValues>>({});
function App() {
return (
<AppContext.Provider
value={{
lang: "de"
}}
>
<ComponentThatUsesAppContext />
</AppContext.Provider>
);
所有值都变为可选,因此在使用它们时必须进行空检查。
键入钩子
大多数钩子不需要任何特殊的键入信息,但可以从用法中推断类型。
useState、useEffect、useContext
useState
、useEffect
和 useContext
都具有泛型类型,因此你不需要额外的注释。下面是一个最小的组件,它使用 useState
,所有类型都从函数签名的默认值中推断出来。
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(() => {
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
的默认对象中获取类型信息。
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
是否实际可用。
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 函数中推断尽可能多的类型。例如,请参阅计数器的 reducer。
// 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 函数,我们就会推断出几个类型并对传递的参数进行类型检查。
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
类型。