JSX 是一種可嵌入的類似 XML 的語法。它旨在轉換為有效的 JavaScript,儘管該轉換的語義是特定於實現的。JSX 隨著 React 框架的流行而興起,但此後也出現了其他實現。TypeScript 支援嵌入、型別檢查並將 JSX 直接編譯為 JavaScript。
基本用法
要使用 JSX,必須完成兩件事。
- 將檔案命名為
.tsx副檔名 - 啟用
jsx選項
TypeScript 附帶了多種 JSX 模式:preserve、react(經典執行時)、react-jsx(自動執行時)、react-jsxdev(自動開發執行時)和 react-native。preserve 模式將 JSX 保留在輸出中,以便後續透過轉換步驟(例如 Babel)進行處理。此外,輸出檔案的副檔名為 .jsx。react 模式會生成 React.createElement,無需在透過 JSX 轉換後即可使用,且輸出檔案的副檔名為 .js。react-native 模式等同於 preserve,它保留所有 JSX,但輸出檔案的副檔名為 .js。
| 模式 | 輸入 | 輸出 | 輸出副檔名 |
|---|---|---|---|
preserve |
<div /> |
<div /> |
.jsx |
react |
<div /> |
React.createElement("div") |
.js |
react-native |
<div /> |
<div /> |
.js |
react-jsx |
<div /> |
_jsx("div", {}, void 0); |
.js |
react-jsxdev |
<div /> |
_jsxDEV("div", {}, void 0, false, {...}, this); |
.js |
你可以使用 jsx 命令列標誌或 tsconfig.json 檔案中的 jsx 選項來指定此模式。
*注意:在使用 react JSX 生成時,你可以透過
jsxFactory選項指定要使用的 JSX 工廠函式(預設為React.createElement)
as 運算子
回顧如何編寫型別斷言
tsconst foo = <Foo>bar;
這斷言變數 bar 具有 Foo 型別。由於 TypeScript 也使用尖括號進行型別斷言,將其與 JSX 的語法結合使用會導致某些解析困難。因此,TypeScript 禁止在 .tsx 檔案中使用尖括號型別斷言。
由於上述語法不能在 .tsx 檔案中使用,因此應使用另一種型別斷言運算子:as。該示例可以輕鬆地用 as 運算子重寫。
tsconst foo = bar as Foo;
as 運算子可在 .ts 和 .tsx 檔案中使用,其行為與尖括號型別斷言風格相同。
型別檢查
為了理解 JSX 的型別檢查,必須首先理解固有元素(intrinsic elements)和基於值的元素(value-based elements)之間的區別。對於給定的 JSX 表示式 <expr />,expr 可能指代環境中的固有內容(例如 DOM 環境中的 div 或 span),或者指代你建立的自定義元件。這一點之所以重要,有兩個原因:
- 對於 React,固有元素作為字串生成(
React.createElement("div")),而你建立的元件則不是(React.createElement(MyComponent))。 - JSX 元素中傳遞的屬性型別查詢方式應當不同。固有元素的屬性應該是固有已知的,而元件通常需要指定它們自己的一組屬性。
TypeScript 使用了與 React 相同的慣例來區分這些元素。固有元素始終以小寫字母開頭,而基於值的元素始終以大寫字母開頭。
JSX 名稱空間
TypeScript 中的 JSX 由 JSX 名稱空間定義。根據 jsx 編譯器選項的不同,JSX 名稱空間可能定義在不同的地方。
jsx 選項 preserve、react 和 react-native 使用經典執行時的型別定義。這意味著需要一個由 jsxFactory 編譯器選項確定的變數在作用域內。JSX 名稱空間應在 JSX 工廠的最頂層識別符號上指定。例如,React 使用預設工廠 React.createElement。這意味著它的 JSX 名稱空間應定義為 React.JSX。
tsexport function createElement(): any;export namespace JSX {// …}
使用者應始終將 React 匯入為 React。
tsimport * as React from 'react';
Preact 使用 JSX 工廠 h。這意味著它的型別應定義為 h.JSX。
tsexport function h(props: any): any;export namespace h.JSX {// …}
使用者應使用命名匯入來匯入 h。
tsimport { h } from 'preact';
對於 jsx 選項 react-jsx 和 react-jsxdev,JSX 名稱空間應從匹配的入口點匯出。對於 react-jsx,這是 ${jsxImportSource}/jsx-runtime。對於 react-jsxdev,這是 ${jsxImportSource}/jsx-dev-runtime。由於這些不使用副檔名,為了支援 ESM 使用者,你必須在 package.json 的 exports 欄位中進行對映。
json{"exports": {"./jsx-runtime": "./jsx-runtime.js","./jsx-dev-runtime": "./jsx-dev-runtime.js",}}
然後在 jsx-runtime.d.ts 和 jsx-dev-runtime.d.ts 中
tsexport namespace JSX {// …}
請注意,雖然匯出 JSX 名稱空間足以進行型別檢查,但生產執行時需要在執行時匯出 jsx、jsxs 和 Fragment,而開發執行時需要 jsxDEV 和 Fragment。理想情況下,你也應該為這些新增型別。
如果 JSX 名稱空間在相應位置不可用,經典執行時和自動執行時都會回退到全域性的 JSX 名稱空間。
固有元素
固有元素在特殊的介面 JSX.IntrinsicElements 上進行查詢。預設情況下,如果未指定此介面,則允許使用任何元素,且不會對固有元素進行型別檢查。但是,如果存在此介面,則固有元素的名稱將作為 JSX.IntrinsicElements 介面上的一個屬性進行查詢。例如
tsxdeclare namespace JSX {interface IntrinsicElements {foo: any;}}<foo />; // ok<bar />; // error
在上面的示例中,<foo /> 可以正常工作,但 <bar /> 將導致錯誤,因為它未在 JSX.IntrinsicElements 上指定。
注意:你也可以在
JSX.IntrinsicElements上指定一個通用的字串索引簽名,如下所示
tsdeclare namespace JSX {interface IntrinsicElements {[elemName: string]: any;}}
基於值的元素
基於值的元素只需透過作用域內的識別符號進行查詢。
tsximport MyComponent from "./myComponent";<MyComponent />; // ok<SomeOtherComponent />; // error
有兩種定義基於值元素的方法:
- 函式元件 (FC)
- 類元件
由於這兩種型別的基於值元素在 JSX 表示式中無法相互區分,TS 首先會嘗試透過過載解析將該表示式解析為函式元件。如果成功,TS 將完成表示式到其宣告的解析。如果該值無法解析為函式元件,TS 將嘗試將其解析為類元件。如果再次失敗,TS 將報告錯誤。
函式元件
顧名思義,元件被定義為 JavaScript 函式,其第一個引數是 props 物件。TS 強制要求其返回型別必須可賦值給 JSX.Element。
tsxinterface FooProp {name: string;X: number;Y: number;}declare function AnotherComponent(prop: { name: string });function ComponentFoo(prop: FooProp) {return <AnotherComponent name={prop.name} />;}const Button = (prop: { value: string }, context: { color: string }) => (<button />);
由於函式元件只是一個 JavaScript 函式,因此這裡也可以使用函式過載
tsTryinterfaceClickableProps {children :JSX .Element [] |JSX .Element ;}interfaceHomeProps extendsClickableProps {home :JSX .Element ;}interfaceSideProps extendsClickableProps {side :JSX .Element | string;}functionMainButton (prop :HomeProps ):JSX .Element ;functionMainButton (prop :SideProps ):JSX .Element ;functionMainButton (prop :ClickableProps ):JSX .Element {// ...}
注意:函式元件以前被稱為無狀態函式元件 (SFC)。由於在最新版本的 React 中,函式元件不再被認為是無狀態的,型別
SFC及其別名StatelessComponent已被棄用。
類元件
可以定義類元件的型別。然而,要做到這一點,最好理解兩個新術語:元素類型別 (element class type) 和 元素例項型別 (element instance type)。
對於給定的 <Expr />,元素類型別就是 Expr 的型別。因此,在上面的例子中,如果 MyComponent 是一個 ES6 類,類型別將是該類的建構函式和靜態成員。如果 MyComponent 是一個工廠函式,類型別將是該函式。
一旦確定了類型別,例項型別就是由該類型別的構造簽名或呼叫簽名的返回型別(以存在的為準)的聯合型別確定的。因此,在 ES6 類的情況下,例項型別將是該類的例項型別;在工廠函式的情況下,它將是該函式返回值的型別。
tsclass MyComponent {render() {}}// use a construct signatureconst myComponent = new MyComponent();// element class type => MyComponent// element instance type => { render: () => void }function MyFactoryFunction() {return {render: () => {},};}// use a call signatureconst myComponent = MyFactoryFunction();// element class type => MyFactoryFunction// element instance type => { render: () => void }
元素例項型別非常重要,因為它必須可賦值給 JSX.ElementClass,否則會導致錯誤。預設情況下 JSX.ElementClass 是 {},但可以透過增強它來限制 JSX 的使用,僅限於符合特定介面的型別。
tsxdeclare namespace JSX {interface ElementClass {render: any;}}class MyComponent {render() {}}function MyFactoryFunction() {return { render: () => {} };}<MyComponent />; // ok<MyFactoryFunction />; // okclass NotAValidComponent {}function NotAValidFactoryFunction() {return {};}<NotAValidComponent />; // error<NotAValidFactoryFunction />; // error
屬性型別檢查
屬性型別檢查的第一步是確定元素屬性型別。這在固有元素和基於值的元素之間略有不同。
對於固有元素,它是 JSX.IntrinsicElements 上屬性的型別。
tsxdeclare namespace JSX {interface IntrinsicElements {foo: { bar?: boolean };}}// element attributes type for 'foo' is '{bar?: boolean}'<foo bar />;
對於基於值的元素,情況稍微複雜一些。它由之前確定的元素例項型別上的某個屬性型別確定。具體使用哪個屬性由 JSX.ElementAttributesProperty 決定。它應宣告為一個包含單個屬性的型別。該屬性的名稱會被使用。從 TypeScript 2.8 開始,如果沒有提供 JSX.ElementAttributesProperty,則將使用類元素建構函式或函式元件呼叫的第一個引數的型別。
tsxdeclare namespace JSX {interface ElementAttributesProperty {props; // specify the property name to use}}class MyComponent {// specify the property on the element instance typeprops: {foo?: string;};}// element attributes type for 'MyComponent' is '{foo?: string}'<MyComponent foo="bar" />;
元素屬性型別用於檢查 JSX 中的屬性。支援可選和必選屬性。
tsxdeclare namespace JSX {interface IntrinsicElements {foo: { requiredProp: string; optionalProp?: number };}}<foo requiredProp="bar" />; // ok<foo requiredProp="bar" optionalProp={0} />; // ok<foo />; // error, requiredProp is missing<foo requiredProp={0} />; // error, requiredProp should be a string<foo requiredProp="bar" unknownProp />; // error, unknownProp does not exist<foo requiredProp="bar" some-unknown-prop />; // ok, because 'some-unknown-prop' is not a valid identifier
注意:如果屬性名稱不是有效的 JS 識別符號(如
data-*屬性),如果它在元素屬性型別中找不到,則不會被視為錯誤。
此外,JSX.IntrinsicAttributes 介面可用於指定 JSX 框架使用的額外屬性,這些屬性通常不由元件的 props 或引數使用——例如 React 中的 key。進一步細分,泛型 JSX.IntrinsicClassAttributes<T> 型別也可用於僅為類元件(而非函式元件)指定此類額外屬性。在此型別中,泛型引數對應於類例項型別。在 React 中,這用於允許 Ref<T> 型別的 ref 屬性。通常來說,這些介面上的所有屬性都應該是可選的,除非你打算讓你的 JSX 框架的使用者在每個標籤上都必須提供某些屬性。
展開運算子 (spread operator) 也可以使用
tsxconst props = { requiredProp: "bar" };<foo {...props} />; // okconst badProps = {};<foo {...badProps} />; // error
子元素 (children) 型別檢查
在 TypeScript 2.3 中,TS 引入了對子元素 (children) 的型別檢查。children 是元素屬性型別中的一個特殊屬性,其中子 JSXExpressions 被視為插入到屬性中。類似於 TS 使用 JSX.ElementAttributesProperty 來確定 props 的名稱,TS 使用 JSX.ElementChildrenAttribute 來確定這些 props 中 children 的名稱。JSX.ElementChildrenAttribute 應宣告為一個包含單個屬性的型別。
tsdeclare namespace JSX {interface ElementChildrenAttribute {children: {}; // specify children name to use}}
tsx<div><h1>Hello</h1></div>;<div><h1>Hello</h1>World</div>;const CustomComp = (props) => <div>{props.children}</div><CustomComp><div>Hello World</div>{"This is just a JS expression..." + 1000}</CustomComp>
你可以像其他任何屬性一樣指定 children 的型別。如果你使用了 React 型別定義,這將覆蓋其中的預設型別。
tsxinterface PropsType {children: JSX.Elementname: string}class Component extends React.Component<PropsType, {}> {render() {return (<h2>{this.props.children}</h2>)}}// OK<Component name="foo"><h1>Hello World</h1></Component>// Error: children is of type JSX.Element not array of JSX.Element<Component name="bar"><h1>Hello World</h1><h2>Hello World</h2></Component>// Error: children is of type JSX.Element not array of JSX.Element or string.<Component name="baz"><h1>Hello</h1>World</Component>
JSX 結果型別
預設情況下,JSX 表示式的結果被標記為 any。你可以透過指定 JSX.Element 介面來自定義該型別。然而,無法從該介面獲取關於 JSX 的元素、屬性或子元素的型別資訊。它是一個黑盒。
JSX 函式返回型別
預設情況下,函式元件必須返回 JSX.Element | null。然而,這並不總是代表執行時行為。從 TypeScript 5.1 開始,你可以指定 JSX.ElementType 來覆蓋什麼是有效的 JSX 元件型別。請注意,這並不定義哪些 props 是有效的。Props 的型別始終由傳入元件的第一個引數定義。預設情況看起來大致如下
tsnamespace JSX {export type ElementType =// All the valid lowercase tags| keyof IntrinsicElements// Function components| (props: any) => Element// Class components| new (props: any) => ElementClass;export interface IntrinsicAttributes extends /*...*/ {}export type Element = /*...*/;export type ElementClass = /*...*/;}
嵌入表示式
JSX 允許你透過使用大括號 ({ }) 包圍表示式,在標籤之間嵌入表示式。
tsxconst a = (<div>{["foo", "bar"].map((i) => (<span>{i / 2}</span>))}</div>);
上面的程式碼會導致錯誤,因為你不能用字串除以數字。使用 preserve 選項時,輸出看起來如下
tsxconst a = (<div>{["foo", "bar"].map(function (i) {return <span>{i / 2}</span>;})}</div>);
React 整合
要在 React 中使用 JSX,你應該使用 React 型別定義。這些型別定義適當地為 React 定義了 JSX 名稱空間。
tsx/// <reference path="react.d.ts" />interface Props {foo: string;}class MyComponent extends React.Component<Props, {}> {render() {return <span>{this.props.foo}</span>;}}<MyComponent foo="bar" />; // ok<MyComponent foo={0} />; // error
配置 JSX
有多個編譯器標誌可用於自定義 JSX,它們既可以作為編譯器標誌,也可以透過每檔案內聯的編譯指令 (pragmas) 使用。要了解更多資訊,請參閱它們的 tsconfig 參考頁面