說明
支持我們

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 處理轉譯。你仍需要指定 jsxFactoryjsxFragmentFactory 以取得正確的類型。

{
  "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

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 的子類型。在以下範例中,我們確保 inputRef 只能傳遞給 HTMLInputElementuseRef 通常會初始化為 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 類型。