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 中有不同的方式來輸入元件。類別元件有泛型型別變數來確保型別安全。只要傳回 JSX,TypeScript 就會將函式視為函式元件。有多種解決方案可為函式元件定義 props。
函式元件
輸入常規函式元件就像在函式引數中新增型別資訊一樣簡單。
interface MyComponentProps {
name: string;
age: number;
};
function MyComponent({ name, age }: MyComponentProps) {
return (
<div>
My name is {name}, I am {age.toString()} years old.
</div>
);
}
您可以在函式簽章中設定預設值來設定預設 props。
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>
);
所有值都會變成選用,因此你在使用它們時必須進行 null 檢查。
輸入掛勾
大多數掛勾不需要任何特殊的輸入資訊,但可以從用法中推斷類型。
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
類型。