TypeScript
Preact は TypeScript 型定義を同梱しており、ライブラリ自体でも使用されています!
TypeScript対応のエディター(VSCodeなど)でPreactを使用すると、通常のJavaScriptを書く際に型情報によるメリットを得られます。独自のアプリケーションに型情報を追加したい場合は、JSDocアノテーションを使用するか、TypeScriptを書いて通常のJavaScriptにトランスパイルすることができます。このセクションでは後者を中心に説明します。
TypeScript設定
TypeScriptには、Babelの代わりに使用できる本格的なJSXコンパイラが含まれています。JSXをPreact互換のJavaScriptにトランスパイルするには、tsconfig.json
に次の設定を追加します。
// 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" }]
],
}
TypeScriptがJSXを正しく解析できるように、.jsx
ファイルを.tsx
に名前変更してください。
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
型です。この型を使用して、独自にchildrenを指定できます。
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
クラスは、2つのジェネリック型変数(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
として型付けされたchildrenが含まれています。
イベントの型付け
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
}
}
たとえば、参照する要素がフォーカスできる入力要素であることを確認したい場合に、これは非常に役立ちます。
コンテキストの型付け
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はリデュース関数からできるだけ多くの型を推論しようとします。たとえば、カウンターのリデュース関数を見てみましょう。
// 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
でリデュース関数を使用すると、いくつかの型を推論し、渡された引数の型チェックを行います。
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>
</>
);
}
必要な注釈は、リデュース関数自体だけです。useReducer
型は、リデュース関数の戻り値がStateType
型であることも保証します。