ヘルプ
サポート

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を使用する場合は、jsxpreserveに設定し、Babelでトランスパイル処理を行います。正しい型を取得するには、jsxFactoryjsxFragmentFactoryを指定する必要があります。

{
  "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型も提供しています。FunctionComponentchildrenの型も追加します。

import { h, FunctionComponent } from "preact";

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

childrenComponentChildren型です。この型を使用して、独自に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

useStateuseEffectuseContextはすべてジェネリック型を使用しているため、追加の注釈は必要ありません。以下は、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のサブタイプにバインドすることからメリットを得ます。下の例では、inputRefHTMLInputElementのみに渡せるようにしています。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型であることも保証します。