一.基本用法
TypeScript 也支持JSX,除了能够像Babel一样把 JSX 编译成 JavaScript 外,还提供了类型检查
只需 2 步,即可使用 TypeScript 写 JSX:
源码文件用
.tsx
扩展名开启
--jsx
选项
此外,TypeScript 提供了 3 种 JSX 处理模式,分别对应不同的代码生成规则:
Mode | Input | Output | Output File Extension |
---|---|---|---|
preserve | <div /> |
<div /> |
.jsx |
react | <div /> |
React.createElement("div") |
.js |
react-native | <div /> |
<div /> |
.js |
也就是说:
preserve:生成
.jsx
文件,但保留 JSX 语法不转换,交给后续构建环节(如Babel)处理react:生成
.js
文件,将 JSX 语法转换成React.createElement
react-native:生成
.js
文件,但保留 JSX 语法不转换
这些模式通过--jsx
选项来指定,默认"preserve"
,只影响代码生成,并不影响类型检查(例如--jsx "preserve"
要求不转换,但仍会对 JSX 进行类型检查)
具体使用上,JSX 语法完全保持一致,唯一需要注意的是类型断言
类型断言
在 JSX 中只能用
as type
(尖括号语法与 JSX 语法冲突)
let someValue: any = "this is a string";
// <type>
let strLength: number = (<string>someValue).length;
在.tsx
文件中会引发报错:
JSX element ‘string’ has no corresponding closing tag.
‘</’ expected.
由于语法冲突,<string>someValue
中的类型断言部分(<string>
)被当成 JSX 元素了。所以在.tsx
中只能使用as type
形式的类型断言:
// as type
let strLength: number = (someValue as string).length;
P.S.关于 TypeScript 类型断言的更多信息,见三.类型断言
二.元素类型
对于一个 JSX 表达式<expr />
,expr
可以是环境中的固有元素(intrinsic element,即内置组件,比如 DOM 环境中的div
或span
),也可以是基于值的元素(value-based element),即自定义组件。两种元素的区别在于:
生成的目标代码不同
React 中,固有元素会生成字符串(比如
React.createElement("div")
),而自定义组件不会(比如React.createElement(MyComponent)
)元素属性(即Props)类型的查找方式不同
固有元素的属性是已知的,而自定义组件可能想要指定自己的属性集
形式上,要求自定义组件必须首字母大写,以此区分两种 JSX 元素
P.S.实际上,固有元素/基于值的元素与内置组件/自定义组件说的是一回事,对 TypeScript 编译器而言,内置组件的类型已知,称之为固有元素,自定义组件的类型与组件声明(值)有关,称之为基于值的元素
固有元素
固有元素的类型从JSX.IntrinsicElements
接口上查找,如果没有声明该接口,那么所有固有元素都不做类型检查,如果声明了,就在JSX.IntrinsicElements
上查找对应的属性,作为类型检查的依据:
declare namespace JSX {
interface IntrinsicElements {
foo: any
}
}
// 正确
<foo />;
// 错误 Property 'bar' does not exist on type 'JSX.IntrinsicElements'.
<bar />;
当然,也可以配合索引签名允许使用未知的内置组件:
declare namespace JSX {
interface IntrinsicElements {
foo: any;
[elemName: string]: any;
}
}
// 正确
<bar />;
好处是将来扩展支持新内置组件后,不需要立即修改类型声明,代价是失去了白名单的严格校验
基于值的元素
基于值的元素直接从作用域里找对应标识符,例如:
import MyComponent from "./myComponent";
// 正确
<MyComponent />
// 错误 Cannot find name 'SomeOtherComponent'.
<SomeOtherComponent />
共有 2 种基于值的元素:
无状态的函数式组件(Stateless Functional Component,所谓 SFC)
类组件(Class Component)
二者单从 JSX 表达式的形式上区分不开,因此先当作 SFC 按照函数重载去尝试解析,解析失败才当类组件处理,还失败就报错
无状态的函数式组件
形式上是个普通函数,要求第一个参数是props
对象,返回类型是JSX.Element
(或其子类型),例如:
function Welcome(props: { name: string }) {
return <h1>Hello, {props.name}</h1>;
}
同样地,函数重载仍然适用:
function Welcome(props: { content: JSX.Element[] | JSX.Element });
function Welcome(props: { name: string });
function Welcome(props: any) {
<h1>Hello, {props.name}</h1>;
}
<div>
<Welcome name="Lily" />
<Welcome content={<span>Hello</span>} />
</div>
P.S.JSX.Element
类型声明来自@types/react
类组件
类组件则继承自React.Component
,与 JavaScript 版没什么区别:
class WelcomeClass extends React.Component {
render() {
return <h1>Hello, there.</h1>;
}
}
<WelcomeClass />
类似于 Class 的双重类型含义,对于 JSX 表达式<Expr />
,类组件的类型分为 2 部分:
元素类类型(element class type):
Expr
的类型,即typeof WelcomeClass
元素实例类型(element instance type):
Expr
类实例的类型,即{ render: () => JSX.Element }
例如:
// 元素类类型
let elementClassType: typeof WelcomeClass;
new elementClassType();
// 元素实例类型
let elementInstanceType: WelcomeClass;
elementInstanceType.render();
要求元素实例类型必须是JSX.ElementClass
的子类型,默认JSX.ElementClass
类型为{}
,在 React 里则限定必须具有render
方法:
namespace JSX {
interface ElementClass extends React.Component<any> {
render(): React.ReactNode;
}
}
(摘自DefinitelyTyped/types/react/index.d.ts)
否则报错:
class NotAValidComponent {}
function NotAValidFactoryFunction() {
return {};
}
<div>
{/* 错误 JSX element type 'NotAValidComponent' is not a constructor function for JSX elements. */}
<NotAValidComponent />
{/* 错误 JSX element type '{}' is not a constructor function for JSX elements. */}
<NotAValidFactoryFunction />
</div>
三.属性类型
属性检查首先要确定元素属性类型(element attributes type),固有元素和基于值的元素在属性类型上存在些许差异:
固有元素的属性类型:
JSX.IntrinsicElements
上对应属性的类型基于值的元素属性类型:元素实例类型上特定属性类型上对应属性的类型,这个特定属性通过
JSX.ElementAttributesProperty
指定
P.S.如果未声明JSX.ElementAttributesProperty
,就取组件类构造函数或 SFC 第一个参数的类型
具体的,固有元素属性以a
的href
为例:
namespace JSX {
interface IntrinsicElements {
// 声明各个固有元素,及其属性类型
a: {
download?: any;
href?: string;
hrefLang?: string;
media?: string;
rel?: string;
target?: string;
type?: string;
referrerPolicy?: string;
}
}
}
// 元素属性类型为 { href?: string }
<a href="">链接</a>
基于值的元素属性例如:
namespace JSX {
// 指定特定属性名为 props
interface ElementAttributesProperty { props: {}; }
}
class MyComponent extends React.Component {
// 声明属性类型
props: {
foo?: string;
}
}
// 元素属性类型为 { foo?: string }
<MyComponent foo="bar" />
可选属性、展开运算符等也同样适用,例如:
class MyComponent extends React.Component {
// 声明属性类型
props: {
requiredProp: string;
optionalProp?: string;
}
}
const props = { optionalProp: 'optional' };
// 正确
<MyComponent { ...props } requiredProp="required" />
P.S.另外,JSX 框架可以通过JSX.IntrinsicAttributes
指定框架所需的额外属性,比如 React 里的key
,具体见Attribute type checking
P.S.特殊的,属性校验只针对属性名为合法 JavaScript 标识符的属性,data-*
之类的不做校验
子组件类型检查
子组件的类型来自元素属性类型上的children
属性,类似于用ElementAttributesProperty
指定props
,这里用JSX.ElementChildrenAttribute
来指定children
:
namespace JSX {
// 指定特定属性名为 children
interface ElementChildrenAttribute { children: {}; }
}
const Wrapper = (props) => (
<div>
{props.children}
</div>
);
<Wrapper>
<div>Hello World</div>
{"This is just a JS expression..." + 1000}
</Wrapper>
给children
指定类型的方式与普通属性类似:
interface PropsType {
children: JSX.Element
name: string
}
class Component extends React.Component<PropsType, {}> {
render() {
return (
<h2>
{this.props.children}
</h2>
)
}
}
<Component name="hello">
<h1>Hello World</h1>
</Component>
子组件类型不匹配会报错:
// 错误 Type '{ children: Element[]; name: string; }' is not assignable to type 'Readonly<PropsType>'.
<Component name="hello">
<h1>Hello World</h1>
<h1>Hi</h1>
</Component>
四.结果类型
默认情况下,一个 JSX 表达式的结果类型是any
:
// a 的类型为 any
let a = <a href="" />;
a = {};
可以通过JSX.Element
来指定,例如 React 中:
let a = <a href="" />;
// 错误 Type '{}' is missing the following properties from type 'Element': type, props, key.
a = {};
对应的类型声明类似于:
namespace JSX {
interface Element<T, P> {
type: T;
props: P;
key: string | number | null;
}
}
P.S.React 里具体的 JSX 元素类型声明见DefinitelyTyped/types/react/index.d.ts
五.嵌入的表达式
JSX 允许在标签内通过花括号语法({ }
)插入表达式:
const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;
(摘自Embedding Expressions in JSX)
TypeScript 同样支持,并且能够对嵌入的表达式做类型检查:
const a = <div>
{/* 错误 The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type. */}
{["foo", "bar"].map(i => <span>{i / 2}</span>)}
</div>
六.结合 React
引入React 类型定义之后,很容易描述 Props 的类型:
interface WelcomeProps {
name: string;
}
// 将 Props 的类型作为第一个类型参数传入
class WelcomeClass extends React.Component<WelcomeProps, {}> {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
// 错误 Property 'name' is missing in type '{}' but required in type 'Readonly<WelcomeProps>'.
let errorCase = <WelcomeClass />;
let correctCase = <WelcomeClass name="Lily" />;
P.S.关于类型参数及泛型的更多信息,见二.类型变量
工厂函数
React 模式(--jsx react
)下,可以配置具体使用的 JSX 元素工厂方法,有 2 种方式:
--jsxFactory
选项:项目级配置内联
@jsx
注释指令:文件级配置
默认为--jsxFactory "React.createElement"
,将 JSX 标签转换为工厂方法调用:
const div = <div />;
// 编译结果
var div = React.createElement("div", null);
在Preact里对应的 JSX 元素工厂方法为h
:
/* @jsx preact.h */
import * as preact from "preact";
<div />;
// 或者
/* @jsx h */
import { h } from "preact";
<div />;
P.S.注意,@jsx
注释指令必须出现在文件首行,其余位置无效
编译结果分别为:
/* @jsx preact.h */
var preact = require("preact");
preact.h("div", null);
// 或者
/* @jsx h */
var preact_1 = require("preact");
preact_1.h("div", null);
P.S.另外,工厂方法配置还会影响 JSX 命名空间的查找,比如默认--jsxFactory React.createElement
的话,优先查找React.JSX
,接下来才看全局JSX
命名空间,如果指定--jsxFactory h
,就优先查找h.JSX
七.总结
TypeScript 中 JSX 的类型支持分为元素类型、属性类型和结果类型 3 部分,如下图: