前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JSX_TypeScript笔记17

JSX_TypeScript笔记17

作者头像
ayqy贾杰
发布2019-06-12 15:17:34
2.2K0
发布2019-06-12 15:17:34
举报
文章被收录于专栏:黯羽轻扬黯羽轻扬

一.基本用法

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 语法冲突)

代码语言:javascript
复制
let someValue: any = "this is a string";
// <type>
let strLength: number = (<string>someValue).length;

.tsx文件中会引发报错:

JSX element ‘string’ has no corresponding closing tag. ‘</’ expected.ts

由于语法冲突,<string>someValue中的类型断言部分(<string>)被当成 JSX 元素了。所以在.tsx中只能使用as type形式的类型断言:

代码语言:javascript
复制
// as type
let strLength: number = (someValue as string).length;

P.S.关于 TypeScript 类型断言的更多信息,见三.类型断言

二.元素类型

对于一个 JSX 表达式<expr />expr可以是环境中的固有元素(intrinsic element,即内置组件,比如 DOM 环境中的divspan),也可以是基于值的元素(value-based element),即自定义组件。两种元素的区别在于:

  • 生成的目标代码不同 React 中,固有元素会生成字符串(比如React.createElement("div")),而自定义组件不会(比如React.createElement(MyComponent)
  • 元素属性(即Props)类型的查找方式不同 固有元素的属性是已知的,而自定义组件可能想要指定自己的属性集

形式上,要求自定义组件必须首字母大写,以此区分两种 JSX 元素

P.S.实际上,固有元素/基于值的元素与内置组件/自定义组件说的是一回事,对 TypeScript 编译器而言,内置组件的类型已知,称之为固有元素,自定义组件的类型与组件声明(值)有关,称之为基于值的元素

固有元素

固有元素的类型从JSX.IntrinsicElements接口上查找,如果没有声明该接口,那么所有固有元素都不做类型检查,如果声明了,就在JSX.IntrinsicElements上查找对应的属性,作为类型检查的依据:

代码语言:javascript
复制
declare namespace JSX {
  interface IntrinsicElements {
    foo: any
  }
}

// 正确
<foo />;
// 错误 Property 'bar' does not exist on type 'JSX.IntrinsicElements'.
<bar />;

当然,也可以配合索引签名允许使用未知的内置组件:

代码语言:javascript
复制
declare namespace JSX {
  interface IntrinsicElements {
    foo: any;
    [elemName: string]: any;
  }
}

// 正确
<bar />;

好处是将来扩展支持新内置组件后,不需要立即修改类型声明,代价是失去了白名单的严格校验

基于值的元素

基于值的元素直接从作用域里找对应标识符,例如:

代码语言:javascript
复制
import MyComponent from "./myComponent";

// 正确
<MyComponent />
// 错误 Cannot find name 'SomeOtherComponent'.
<SomeOtherComponent />

共有 2 种基于值的元素:

  • 无状态的函数式组件(Stateless Functional Component,所谓 SFC)
  • 类组件(Class Component)

二者单从 JSX 表达式的形式上区分不开,因此先当作 SFC 按照函数重载去尝试解析,解析失败才当类组件处理,还失败就报错

无状态的函数式组件

形式上是个普通函数,要求第一个参数是props对象,返回类型是JSX.Element(或其子类型),例如:

代码语言:javascript
复制
function Welcome(props: { name: string }) {
  return <h1>Hello, {props.name}</h1>;
}

同样地,函数重载仍然适用:

代码语言:javascript
复制
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 版没什么区别:

代码语言: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 }

例如:

代码语言:javascript
复制
// 元素类类型
let elementClassType: typeof WelcomeClass;
new elementClassType();
// 元素实例类型
let elementInstanceType: WelcomeClass;
elementInstanceType.render();

要求元素实例类型必须是JSX.ElementClass的子类型,默认JSX.ElementClass类型为{},在 React 里则限定必须具有render方法:

代码语言:javascript
复制
namespace JSX {
  interface ElementClass extends React.Component<any> {
    render(): React.ReactNode;
  }
}

(摘自DefinitelyTyped/types/react/index.d.ts)

否则报错:

代码语言:javascript
复制
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 第一个参数的类型

具体的,固有元素属性以ahref为例:

代码语言:javascript
复制
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>

基于值的元素属性例如:

代码语言:javascript
复制
namespace JSX {
  // 指定特定属性名为 props
  interface ElementAttributesProperty { props: {}; }
}

class MyComponent extends React.Component {
  // 声明属性类型
  props: {
    foo?: string;
  }
}
// 元素属性类型为 { foo?: string }
<MyComponent foo="bar" />

可选属性、展开运算符等也同样适用,例如:

代码语言:javascript
复制
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

代码语言:javascript
复制
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指定类型的方式与普通属性类似:

代码语言:javascript
复制
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>

子组件类型不匹配会报错:

代码语言:javascript
复制
// 错误 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

代码语言:javascript
复制
// a 的类型为 any
let a = <a href="" />;
a = {};

可以通过JSX.Element来指定,例如 React 中:

代码语言:javascript
复制
let a = <a href="" />;
// 错误 Type '{}' is missing the following properties from type 'Element': type, props, key.
a = {};

对应的类型声明类似于:

代码语言:javascript
复制
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 允许在标签内通过花括号语法({ })插入表达式:

代码语言:javascript
复制
const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;

(摘自Embedding Expressions in JSX)

TypeScript 同样支持,并且能够对嵌入的表达式做类型检查:

代码语言:javascript
复制
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 的类型:

代码语言:javascript
复制
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 标签转换为工厂方法调用:

代码语言:javascript
复制
const div = <div />;
// 编译结果
var div = React.createElement("div", null);

在Preact里对应的 JSX 元素工厂方法为h

代码语言:javascript
复制
/* @jsx preact.h */
import * as preact from "preact";
<div />;
// 或者
/* @jsx h */
import { h } from "preact";
<div />;

P.S.注意,@jsx注释指令必须出现在文件首行,其余位置无效

编译结果分别为:

代码语言:javascript
复制
/* @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 部分,如下图:

参考资料

  • JSX
  • TypeScript 2.8: Per-File JSX Factories
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-04-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端向后 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一.基本用法
    • 类型断言
    • 二.元素类型
      • 固有元素
        • 基于值的元素
          • 无状态的函数式组件
          • 类组件
      • 三.属性类型
        • 子组件类型检查
        • 四.结果类型
        • 五.嵌入的表达式
        • 六.结合 React
          • 工厂函数
          • 七.总结
            • 参考资料
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档