blog icon indicating copy to clipboard operation
blog copied to clipboard

[翻译] TypeScript 和 JSX 搞基的事

Open techird opened this issue 9 years ago • 16 comments

之前的一篇翻译安利了一下 TypeScript,今天再给大家翻译一篇 TypeScript 和 JSX 结合使用的安利文。 原文地址:http://www.jbrantly.com/typescript-and-jsx/

对 JSX 的支持已经在 TypeScript 官方落地了!非常感谢 Ryan CavanaughFrançois de Campredon 的大力推动。在这篇文章中,笔者打算跟大家探索一下如何把 JSX 和 TypeScript 的第一特性——静态类型检查完美结合使用。

发展历史

当我们开始在 AssureSign 项目中实验性地使用 React 的时候,也开始在使用 TypeScript 了。然而当我们碰到 JSX 的时候,一道石墙突然在我们面前挡道。在 Github 中已经有一个历史悠久的 Issue 去反映这个问题,然而一直没有得出实质性的解决方案,或者干脆让你“不要使用 JSX”了。这对我来说不可接受,所以我用了一点黑魔法来解决这个问题。虽然可以玩起来,但这个方法太挫了,并且没有类型检查。François de Campredon 后来创建了一个 jsx-typescript 的项目来证明其实 TypeScript 是可以支持 JSX 的。然后,很突然,会得到 TypeScript 官方支持的消息不胫而走。三个月后,它就真的支持了。

安装

~~目前 JSX 还没有稳定版本,所以你需要去拉取最新代码。~~ JSX 已经在 TypeScript 1.6 以上版本支持。

$ npm install typescript

基础使用

使用 JSX 之前,你需要做两件事情:

  1. 文件使用 .tsx 作为扩展名
  2. 开启 TypeScript 的 jsx 选项

TypeScript 发布了两种 JSX 的工作模式:preserve 模式和 react 模式。这两个模式只影响编译策略。preserve 模式会保留 JSX 文件,以便日后进一步被 babel 使用,输出的文件是 .jsx 格式的。而 react 模式则会直接编译成 React.createElement,在使用之前就不需要再做 JSX 转换了,输出的文件是 .js 格式的。

模式 输入 输出
preserve <div/> <div/>
react <div/> React.createElement("div")

你可以在命令行中使用 --jsx 参数或者在 tsconfig.json 中指定工作模式。

注意,编译生成的代码中,React 关键字是硬编码的,所以要保证全局变量中的 React 可用,并且 R 是大写的,react 就不行了。

as 运算符

由于 TypeScript 使用尖括号做类型声明,那么在编译的时候,类型声明会和 JSX 的语法冲突。比如以下代码:

var foo1 = <foo>bar
</foo>

这个代码是准备创建一个 JSX 元素,还是想要声明 bar 变量是 foo 类型的?为了让这个问题简化,.tsx 文件里不支持尖括号类型声明语法。所以如果上面的代码是在 .tsx 文件里的话,表示的是创建了一个 JSX 元素,而如果在 .ts 文件里,就会报错。为了弥补 .tsx 文件中类型声明语法的缺失,添加了一个新的类型声明操作符:as

var foo1 = bar as foo;

as 操作符在 .ts 文件和 .tsx 文件中都是可用的。

类型检查

要是没有类型检查,JSX 和 TypeScript 搞基有什么意义?多亏有了完整的类型检查,世界变得精彩纷呈。

首先,要区别内置元素和自定义元素。给定一个 JSX 表达式 <expr />,这个 expr 到底该对应环境里的一个内置元素(比如 DOM 环境下的 div 或者 span)还是应该对应一个自己写的自定义组件?区分这单很重要,因为:

  1. 对于 React 来说,内置元素会编译成字符串,比如 React.createComponent('div'),而自定义元素则不是:React.createElement(MyComponent)
  2. 传递给 JSX 标签的属性类型的查找方式是不同的。内置元素支持的属性应该是内置的,而自定义组件的属性取决于 props 的定义。

对于这两者,TypeScript 和 React 使用同样的区分方式:内置元素的标签以小写字母开头,自定义元素的标签使用大写字母开头。

内置元素

内置元素会在接口 JSX.InterinsicElements 中定义。默认情况下,如果这个接口并没有定义,那么所有的内置元素都不会进行类型检查。然而,如果你定义了这个接口,那么内置元素将会使用在接口属性中定义的类型。

declare module JSX {  
    interface IntrinsicElements {
        foo: any
    }
}

<foo />; // ok  
<bar />; // error  

在上述示例中,foo 可以通过类型检查但是 bar 不行。因为 bar 在内置元素接口属性中没有定义。

注意:你也可以在 JSX.IntrinsicElements 接口中定义一个字符串索引器来匹配任意的内置元素

笔者注:JSX.IntrinsicElements 在 Dom 环境中的定义已经在 React 的 DefinitelyTyped 中描述好了。React 环境下可直接使用。

自定义元素

自定义元素可以简单地通过标识符来查找到。

import MyComponent = require('./myComponent');

<MyComponent />; // ok  
<SomeOtherComponent />; // error  

我们是可以限制自定义组件的类型的。然而,为了说明白,我们先要介绍两个概念:_组件类类型(Element Class Type)和_组件实例类型(Element Instance Type)

组件类类型很简单,对于 <Expr> 组件,其类类型就是 Expr 的类型。所以在上面的例子中,如果 MyCompoent 是一个 ES6 的类,那么 <MyComponent> 的类类型就是这个类的类型。如果 MyComponent 是一个工厂方法,那么 <MyCompoent> 的类类型就是这个方法的类型。

一旦元素类类型确定了,元素的实例类型就由类的调用签名和构造签名共同决定。在看上述例子,如果 MyComponent 是一个 ES6 的类,那么实例类型就是这个类的实例的类型,如果是个工厂方法,实例类型则是这个方法的返回值。是不是有点绕?我们来看看下面这个例子:

class MyComponent {  
  render() {}
}

// 使用构造函数
var myComponent = new MyComponent();

// 组件类类型 => MyComponent
// 元素实例类型 => { render: () => void }

function MyFactoryFunction() {  
  return { 
    render: () => {
    } 
  }
}

// 使用工厂方法调用
var myComponent = MyFactoryFunction();

// 组件类类型 => MyFactoryFunction
// 组件实例类型 => { render: () => void }

现在组件实例类型是一个有趣的类型,它要求满足 JSX.ElementClass 的接口,否则将会报错。默认情况下,JSX.ElementClass 就是 {},不过我们是可以人为增加限制。

declare module JSX {  
  interface ElementClass {
    render: any;
  }
}

class MyComponent {  
  render() {}
}
function MyFactoryFunction() {  
  return { render: () => {} }
}

<MyComponent />; // ok  
<MyFactoryFunction />; // ok

class NotAValidComponent {}  
function NotAValidFactoryFunction() {  
    return {};
}

<NotAValidComponent />; // error  
<NotAValidFactoryFunction />; // error  

译者注:JSX.ElementClass 同样在 React 的 DefinitelyTyped 中定义好了,大家通过 tsd 或者 nuget 可以下载下来使用

属性类型检查

做属性的类型检查,第一步就是要确定_元素属性类型_。对于内置元素和自定义元素,确定方式有一些区别。

对于内置组件,组件属性类型就是 JSX.IntrinsicElements 上的属性类型。

declare module JSX {  
  interface IntrinsicElements {
    foo: { bar?: boolean }
  }
}

// `foo` 的元素属性类型是 `{bar?: boolean}`
<foo bar />;  

对于自定义组件,情况复杂一点点。它的类型是之前讨论的_组件实例类型_的一个属性决定的。你问我是哪个属性?哈哈,你可以自己决定!在 JSX.ElementAttributesProperty 上定义唯一一个属性,这个属性就是决定自定义组件属性类型的属性。(译者:好绕,还是看示例)

declare module JSX {  
  interface ElementAttributesProperty {
    props; // 指定使用哪个属性名称
  }
}

class MyComponent {  
  // 在元素实例类型上定义这个属性
  props: {
    foo?: string;
  }
}

// `MyComponent` 的元素属性类型就是 `{foo?: string}`
<MyComponent foo="bar" />  

上面的例子可以明显看到,元素属性类型就是用于对 JAX 的元素做类型检查的。支持可选和必选属性。

declare module 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-_ 属性),那么这个属性在进行类型检查的时候不会认为是错误,而是直接跳过。*

属性集也是支持的:

var props = { requiredProp: 'bar' };  
<foo {...props} />; // ok

var badProps = {};  
<foo {...badProps} />; // error  

JSX 的类型

好了,现在我们可以写 JSX 并且有了元素和属性的类型检查,但是 JSX 本身的类型是什么呢?默认来说,JSX 的类型是 any。你可以通过 JSX.Element 接口自定义这个类型。然而,我们从这个接口是不可能知道元素、元素的属性或者子节点的类型信息的。这是个黑盒。

内嵌 TypeScript

JSX 在 Javascript 里允许通过花括号 { } 内嵌 JavaScript 代码。JSX 在 TypeScript 里同样允许你这样做,只不过是内嵌 TypeScript 代码。这就意味着这些 JSX 内嵌的 TypeScript 代码同样支持转译功能以及类型检查。

var a = <div>  
  {['foo', 'bar'].map(i => <span>{i/2}</span>)}
</div>

上面的代码会报错,因为你不能使用字符串来除以一个数值。当使用 preserve 模式的时候,输出是这样的:

var a = <div>  
  {['foo', 'bar'].map(function (i) { return <span>{i / 2}</span>; })}
</div>

React 集成

TypeScript 对 JSX 的支持的设计是不考虑其使用方式的。然而,React 始终是主要的使用方。我年初做过一个关于集成 React 和 TypeScript 的演讲。很多观点如今依然适用。React 的 DefiniteTyped 仓库的最近更新中集成了 JSX.IntrinsicElementsJSX.ElementAttributeProperty 的概念。

/// <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  

更多资源

这篇文章中的很多信息都来自 TypeScript 的 Issue #3203。不过,这个讨论已经持续了很长时间并且扩散到很多个地方。我调了几个个人认为值得深挖的:

  • http://typescript.codeplex.com/workitem/2608
  • https://github.com/facebook/react/issues/759
  • https://github.com/Microsoft/TypeScript/issues/296
  • https://github.com/Microsoft/TypeScript/pull/3201
  • https://github.com/Microsoft/TypeScript/issues/3203
  • https://github.com/Microsoft/TypeScript/pull/3564

译者注:转载本文请保留出处:https://github.com/techird/blog/issues/3

techird avatar Nov 04 '15 05:11 techird

棒棒的

puterjam avatar Nov 04 '15 12:11 puterjam

点赞~

hefangshi avatar Nov 06 '15 02:11 hefangshi

值得关注

tyzero avatar Dec 22 '15 03:12 tyzero

看的我热血沸腾。。。

yahue avatar Jan 12 '16 08:01 yahue

请教一下,楼主有没有关于typescript和react的代码组织结构方面的文章。比如说我在项目开发中要用redux、react-router,要引用react库、jquery库,这些库想编译成一个js文件,还有若干公共React组件,也会分类编译成几个js文件,还有一些页面react的js文件,是一对一的编译。使用过多个解决方案,始终没有完美的,也可能是我技术不够,比如tsc原生编译的js文件中require方法不能在浏览器中用,用gulp和gulp-typescript,不识别模块,使用browserify也不行,要再加上babel,那不用ts也能转译react语法了,感觉ts好像没用上。另外ts用的库都在typings里,还要保证翻译后的js或jsx被其他的构建系统和转换工具所识别,而且网上的教程多是如何把所有的tsx编译成一个js,我不可能那么做啊,那样的话,js文件得多大啊!好折腾,每个方案都只是差一点,就是不能实现。

xieyanlei avatar Apr 29 '16 02:04 xieyanlei

@xieyanlei 可以参考我的这个项目:https://github.com/techird/react-redux-typescript-start

techird avatar May 10 '16 04:05 techird

非常感谢楼主的辛苦工作,我会仔细学习的,如果有什么不清楚的地方再向你请教。说句题外话,你能给个思路点拨一下,就非常好了,没想到还专门给写了代码,不胜感激哈!

xieyanlei avatar May 10 '16 04:05 xieyanlei

真是棒!TypeScript + React 才是未来啊、

JimmyLv avatar Sep 23 '16 09:09 JimmyLv

我表示,我懵逼!tsx让我要先学两种写法才能看懂ant design mobile的代码...不禁留下了眼泪...

hifizz avatar Apr 26 '17 10:04 hifizz

楼主,请教一下 在tsconfig.json中配置的paths在vscode中不识别 但是ts编译能通过 怎么解决啊。。

zpbc007 avatar Dec 26 '17 01:12 zpbc007

interface ElementAttributesProperty {
    props; // 指定使用哪个属性名称
  }

为什么这里的props可以没有值。我也没在官网文档上找到这样的写法。。。

terminalqo avatar Jul 03 '18 05:07 terminalqo

还有,这里的

// 使用构造函数
var myComponent = new MyComponent();

new 是什么意思, React 中并不需要 new 啊。作为类我知道新建实例需要new,但是react中并不需要吧,如果说MyComponent 并不是react中的,那为什么里面会有render函数?

terminalqo avatar Jul 03 '18 09:07 terminalqo

好文!收藏!!!

hkongm avatar Aug 01 '18 03:08 hkongm

我能不能不改jsx后缀,在项目中同时使用jsx和tsx,因为项目是很多人一起做的,我只负责了一部分,这种情况我使用tsx不能影响其他人的,这样我先拿自己的部分做实验,然后推广

callmedanieldaniel avatar Aug 10 '18 10:08 callmedanieldaniel

请问楼主,怎么像使用jsx一样使用defaultProps给组件的props设置默认值呢?

helios741 avatar Aug 12 '18 13:08 helios741

默认

any一把梭

LoveMuZiLi avatar Jan 16 '23 08:01 LoveMuZiLi