JSX 是一種可嵌入的類似 XML 的語法。它用於轉換成有效的 JavaScript,儘管轉換的語意取決於實作。JSX 因 React 框架而廣受歡迎,但後來也出現了其他實作。TypeScript 支援將 JSX 直接嵌入、檢查類型和編譯成 JavaScript。
基本用法
要使用 JSX,您必須執行兩件事。
- 使用
.tsx
副檔名命名檔案 - 啟用
jsx
選項
TypeScript 附帶三種 JSX 模式:preserve
、react
和 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
命令列旗標或對應的選項 jsx
in your tsconfig.json 檔案來指定此模式。
*注意:使用
jsxFactory
選項(預設為React.createElement
)可以指定在針對 react JSX 發射時要使用的 JSX 工廠函式。
as
算子
回想如何撰寫類型斷言
ts
const foo = <foo>bar;
這斷言變數 bar
具有類型 foo
。由於 TypeScript 也使用尖括號作為類型斷言,將其與 JSX 的語法結合使用會造成某些解析困難。因此,TypeScript 不允許在 .tsx
檔案中使用尖括號類型斷言。
由於無法在 .tsx
檔案中使用上述語法,因此應該使用替代的類型斷言運算子:as
。可以輕鬆使用 as
運算子改寫範例。
ts
const foo = bar as foo;
as
運算子在 .ts
和 .tsx
檔案中均可用,且其行為與尖括號類型斷言樣式相同。
類型檢查
為了瞭解 JSX 的類型檢查,您必須先瞭解內建元素和基於值的元素之間的差異。給定 JSX 表達式 <expr />
,expr
可能參照環境中的內建元素(例如 DOM 環境中的 div
或 span
)或您建立的客製化元件。這很重要,原因有二
- 對於 React,內建元素會以字串發射(
React.createElement("div")
),而您建立的元件則不會(React.createElement(MyComponent)
)。 - 在 JSX 元素中傳遞的屬性類型應該以不同的方式查詢。內建元素屬性應該內建已知,而元件可能會想要指定自己的屬性集。
TypeScript 使用 與 React 相同的慣例 來區分這些。內建元素總是從小寫字母開始,而基於值的元素總是從大寫字母開始。
內建元素
內建元素會在特殊介面 JSX.IntrinsicElements
上查詢。預設情況下,如果未指定此介面,則任何內容都不會受到類型檢查。但是,如果存在此介面,則會將內建元素的名稱作為 JSX.IntrinsicElements
介面上的屬性進行查詢。例如
ts
declare namespace JSX {interface IntrinsicElements {foo: any;}}<foo />; // ok<bar />; // error
在以上範例中,<foo />
會正常運作,但 <bar />
會導致錯誤,因為它未在 JSX.IntrinsicElements
上指定。
注意:您也可以在
JSX.IntrinsicElements
上指定萬用字串索引器,如下所示
ts
declare namespace JSX {interface IntrinsicElements {[elemName: string]: any;}}
基於值的元素
基於值的元素僅透過範圍內的識別碼查詢。
ts
import MyComponent from "./myComponent";<MyComponent />; // ok<SomeOtherComponent />; // error
有兩種方法可以定義基於值的元素
- 函式元件 (FC)
- 類別元件
由於這兩種基於值的元素類型在 JSX 表達式中無法區分,因此 TS 會先嘗試使用重載解析來將表達式解析為函式元件。如果處理程序成功,則 TS 會完成將表達式解析為其宣告。如果無法將值解析為函式元件,TS 會嘗試將其解析為類別元件。如果失敗,TS 會回報錯誤。
函式元件
如同名稱所示,元件定義為 JavaScript 函式,其第一個引數是 props
物件。TS 強制執行其傳回類型必須可指派給 JSX.Element
。
ts
interface 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 函式,因此函式重載也可以在此使用
tsTry
interfaceClickableProps {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
已棄用。
類別元件
可以定義類別元件的類型。不過,為此最好先了解兩個新名詞:元素類別類型和元素實例類型。
針對 <Expr />
,元素類別類型是 Expr
的類型。因此在上述範例中,如果 MyComponent
是 ES6 類別,類別類型會是該類別的建構函式和靜態方法。如果 MyComponent
是工廠函式,類別類型會是該函式。
建立類別類型後,實例類型會由類別類型的建構或呼叫簽章的回傳類型聯集決定(視實際情況而定)。因此,在 ES6 類別的情況下,實例類型會是該類別實例的類型,在工廠函式的狀況下,則會是函式回傳值的類型。
ts
class 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 的使用,僅限於符合適當介面的類型。
ts
declare 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
上的屬性類型
ts
declare namespace JSX {interface IntrinsicElements {foo: { bar?: boolean };}}// element attributes type for 'foo' is '{bar?: boolean}'<foo bar />;
對於基於值的元素,它稍微複雜一點。它是先前確定的元素實例類型上屬性類型的類型。要使用哪個屬性是由 JSX.ElementAttributesProperty
確定的。它應該用單一屬性宣告。然後使用該屬性的名稱。在 TypeScript 2.8 中,如果未提供 JSX.ElementAttributesProperty
,則會改用類別元素建構函式或函式元件呼叫的第一個參數的類型。
ts
declare 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 中的屬性。支援選擇性和必要屬性。
ts
declare 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
屬性為 Ref<T>
類型。一般來說,這些介面上的所有屬性都應該是選用的,除非您打算讓 JSX 架構的使用者需要在每個標籤上提供一些屬性。
擴散運算子也適用
ts
const props = { requiredProp: "bar" };<foo {...props} />; // okconst badProps = {};<foo {...badProps} />; // error
子項類型檢查
在 TypeScript 2.3 中,TS 引入了對 子項 的類型檢查。子項 是 元素屬性類型 中的一個特殊屬性,其中子項 JSXExpression 會插入到屬性中。類似於 TS 使用 JSX.ElementAttributesProperty
來確定 props 的名稱,TS 使用 JSX.ElementChildrenAttribute
來確定這些 props 中 子項 的名稱。JSX.ElementChildrenAttribute
應宣告為單一屬性。
ts
declare namespace JSX {interface ElementChildrenAttribute {children: {}; // specify children name to use}}
ts
<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>
您可以指定 子項 的類型,就像其他屬性一樣。如果您使用它們,這將覆寫預設類型,例如 React 編寫 中的類型。
ts
interface 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 允許你在標籤之間內嵌表達式,方法是使用大括號 ({ }
) 將表達式括起來。
ts
const a = (<div>{["foo", "bar"].map((i) => (<span>{i / 2}</span>))}</div>);
由於無法將字串除以數字,因此以上的程式碼會產生錯誤。當使用 preserve
選項時,輸出看起來像
ts
const a = (<div>{["foo", "bar"].map(function (i) {return <span>{i / 2}</span>;})}</div>);
React 整合
若要將 JSX 與 React 搭配使用,您應該使用 React 打字。這些打字會適當地定義 JSX
名稱空間,以供與 React 搭配使用。
ts
/// <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,它們同時作為編譯器旗標和透過內嵌每個檔案實用指令運作。若要深入了解,請參閱其 tsconfig 參考頁面