Skip to main content

JSX_TypeScript Notes 17

Free2019-04-27#TypeScript#tsx#TypeScript JSX#tsx props type#TypeScript React#tsx 类型检查

TS also provides complete type support for JSX

1. Basic Usage

TypeScript also supports JSX, besides being able to compile JSX to JavaScript like Babel, also provides type checking

Only need 2 steps to use TypeScript to write JSX:

  • Source files use .tsx extension

  • Enable --jsx option

Additionally, TypeScript provides 3 JSX processing modes, corresponding to different code generation rules:

ModeInputOutputOutput File Extension
preserve<div /><div />.jsx
react<div />React.createElement("div").js
react-native<div /><div />.js

That is:

  • preserve: Generate .jsx files, but preserve JSX syntax without conversion, hand over to subsequent build steps (such as Babel) to handle

  • react: Generate .js files, convert JSX syntax to React.createElement

  • react-native: Generate .js files, but preserve JSX syntax without conversion

These modes are specified through --jsx option, defaults "preserve", only affects code generation, doesn't affect type checking (for example --jsx "preserve" requires no conversion, but still performs type checking on JSX)

In specific usage, JSX syntax is completely consistent, only thing to note is type assertions

Type Assertions

In JSX can only use as type (angle bracket syntax conflicts with JSX syntax)

let someValue: any = "this is a string";
// <type>
let strLength: number = (<string>someValue).length;

In .tsx files will trigger error:

JSX element 'string' has no corresponding closing tag.

'</' expected.

Due to syntax conflict, type assertion part in <string>someValue (<string>) is treated as JSX element. So in .tsx can only use as type form of type assertion:

// as type
let strLength: number = (someValue as string).length;

P.S. For more information about TypeScript type assertions, see [3. Type Assertions](/articles/基本类型-typescript 笔记 2/#articleHeader4)

2. Element Types

For a JSX expression <expr />, expr can be intrinsic element in environment (intrinsic element, i.e. built-in components, such as div or span in DOM environment), or can be value-based element, i.e. custom components. Difference between two types of elements lies in:

  • Generated target code is different

    In React, intrinsic elements generate strings (such as React.createElement("div")), while custom components don't (such as React.createElement(MyComponent))

  • Element property (i.e. Props) type lookup method is different

    Intrinsic element properties are known, while custom components may want to specify their own property sets

In form, requires custom components must have first letter capitalized, to distinguish two types of JSX elements

P.S. Actually, intrinsic element/value-based element and built-in component/custom component are talking about same thing, for TypeScript compiler, built-in component types are known, called intrinsic elements, custom component types are related to component declaration (value), called value-based elements

Intrinsic Elements

Intrinsic element types are looked up from JSX.IntrinsicElements interface, if this interface is not declared, then all intrinsic elements don't do type checking, if declared, look up corresponding properties on JSX.IntrinsicElements, as basis for type checking:

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

// Correct
<foo />;
// Error Property 'bar' does not exist on type 'JSX.IntrinsicElements'.
<bar />;

Of course, can also cooperate with index signature to allow using unknown built-in components:

declare namespace JSX {
  interface IntrinsicElements {
    foo: any;
    [elemName: string]: any;
  }
}

// Correct
<bar />;

Benefit is after extending support for new built-in components in future, don't need to immediately modify type declarations, cost is losing strict validation of whitelist

Value-Based Elements

Value-based elements directly find corresponding identifiers from scope, for example:

import MyComponent from "./myComponent";

// Correct
<MyComponent />
// Error Cannot find name 'SomeOtherComponent'.
<SomeOtherComponent />

There are 2 types of value-based elements:

  • Stateless Functional Component (so-called SFC)

  • Class Component

These two cannot be distinguished from JSX expression form alone, therefore first try to parse as SFC according to function overloads, if parsing fails then handle as class component, if still fails then error

Stateless Functional Components

In form is a normal function, requires first parameter is props object, return type is JSX.Element (or its [subtype](/articles/深入类型系统-typescript 笔记 8/#articleHeader4)), for example:

function Welcome(props: { name: string }) {
  return <h1>Hello, {props.name}</h1>;
}

Similarly, [function overloads](/articles/函数-typescript 笔记 5/#articleHeader9) still apply:

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 type declaration comes from @types/react

Class Components

Class components inherit from React.Component, no difference from JavaScript version:

class WelcomeClass extends React.Component {
  render() {
    return <h1>Hello, there.</h1>;
  }
}

<WelcomeClass />

Similar to Class's [dual type meanings](/articles/类-typescript 笔记 4/#articleHeader8), for JSX expression <Expr />, class component types are divided into 2 parts:

  • Element class type: Expr's type, i.e. typeof WelcomeClass

  • Element instance type: Expr class instance's type, i.e. { render: () => JSX.Element }

For example:

// Element class type
let elementClassType: typeof WelcomeClass;
new elementClassType();
// Element instance type
let elementInstanceType: WelcomeClass;
elementInstanceType.render();

Requires element instance type must be subtype of JSX.ElementClass, defaults JSX.ElementClass type is {}, in React then limits must have render method:

namespace JSX {
  interface ElementClass extends React.Component<any> {
    render(): React.ReactNode;
  }
}

(From DefinitelyTyped/types/react/index.d.ts)

Otherwise error:

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

<div>
  {/* Error JSX element type 'NotAValidComponent' is not a constructor function for JSX elements. */}
  <NotAValidComponent />
  {/* Error JSX element type '{}' is not a constructor function for JSX elements. */}
  <NotAValidFactoryFunction />
</div>

3. Property Types

Property checking first needs to determine element attribute type (element attributes type), intrinsic elements and value-based elements have slight differences in property types:

  • Intrinsic element property type: Type of corresponding property on JSX.IntrinsicElements

  • Value-based element property type: Type of corresponding property on specific property type on element instance type, this specific property is specified through JSX.ElementAttributesProperty

P.S. If JSX.ElementAttributesProperty is not declared, take component class constructor or SFC first parameter's type

Specifically, intrinsic element properties taking a's href as example:

namespace JSX {
  interface IntrinsicElements {
    // Declare various intrinsic elements, and their property types
    a: {
      download?: any;
      href?: string;
      hrefLang?: string;
      media?: string;
      rel?: string;
      target?: string;
      type?: string;
      referrerPolicy?: string;
    }
  }
}

// Element property type is { href?: string }
<a href="">链接</a>

Value-based element properties for example:

namespace JSX {
  // Specify specific property name as props
  interface ElementAttributesProperty { props: {}; }
}

class MyComponent extends React.Component {
  // Declare property type
  props: {
    foo?: string;
  }
}
// Element property type is { foo?: string }
<MyComponent foo="bar" />

Optional properties, spread operators etc. also apply, for example:

class MyComponent extends React.Component {
  // Declare property type
  props: {
    requiredProp: string;
    optionalProp?: string;
  }
}

const props = { optionalProp: 'optional' };
// Correct
<MyComponent { ...props } requiredProp="required" />

P.S. Additionally, JSX frameworks can specify framework's required extra properties through JSX.IntrinsicAttributes, such as key in React, specifically see Attribute type checking

P.S. Specially, property validation only targets properties with property names being legal JavaScript identifiers, data-* etc. don't do validation

Child Component Type Checking

Child component types come from children property on element property type, similar to using ElementAttributesProperty to specify props, here use JSX.ElementChildrenAttribute to specify children:

namespace JSX {
  // Specify specific property name as children
  interface ElementChildrenAttribute { children: {}; }
}

const Wrapper = (props) => (
  <div>
    {props.children}
  </div>
);
<Wrapper>
  <div>Hello World</div>
  {"This is just a JS expression..." + 1000}
</Wrapper>

Method to specify children type is similar to normal properties:

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>

Child component type mismatch will error:

// Error Type '{ children: Element[]; name: string; }' is not assignable to type 'Readonly<PropsType>'.
<Component name="hello">
  <h1>Hello World</h1>
  <h1>Hi</h1>
</Component>

4. Result Types

By default, a JSX expression's result type is any:

// a's type is any
let a = <a href="" />;
a = {};

Can specify through JSX.Element, for example in React:

let a = <a href="" />;
// Error Type '{}' is missing the following properties from type 'Element': type, props, key.
a = {};

Corresponding type declaration is similar to:

namespace JSX {
  interface Element<T, P> {
    type: T;
    props: P;
    key: string | number | null;
  }
}

P.S. React's specific JSX element type declarations see DefinitelyTyped/types/react/index.d.ts

5. Embedded Expressions

JSX allows inserting expressions through curly brace syntax ({ }) inside tags:

const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;

(From Embedding Expressions in JSX)

TypeScript also supports, and can do type checking on embedded expressions:

const a = <div>
  {/* Error 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>

6. Combining with React

After introducing React type definitions, easy to describe Props types:

interface WelcomeProps {
  name: string;
}
// Pass Props type as first type parameter
class WelcomeClass extends React.Component<WelcomeProps, {}> {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}
// Error Property 'name' is missing in type '{}' but required in type 'Readonly<WelcomeProps>'.
let errorCase = <WelcomeClass />;
let correctCase = <WelcomeClass name="Lily" />;

P.S. For more information about type parameters and generics, see [2. Type Variables](/articles/泛型-typescript 笔记 6/#articleHeader3)

Factory Functions

Under React mode (--jsx react), can configure specific JSX element factory method to use, has 2 ways:

  • --jsxFactory option: Project-level configuration

  • Inline @jsx comment directive: File-level configuration

Defaults to --jsxFactory "React.createElement", convert JSX tags to factory method calls:

const div = <div />;
// Compilation result
var div = React.createElement("div", null);

In Preact corresponding JSX element factory method is h:

/* @jsx preact.h */
import * as preact from "preact";
<div />;
// Or
/* @jsx h */
import { h } from "preact";
<div />;

P.S. Note, @jsx comment directive must appear on file's first line, other positions are invalid

Compilation results respectively:

/* @jsx preact.h */
var preact = require("preact");
preact.h("div", null);
// Or
/* @jsx h */
var preact_1 = require("preact");
preact_1.h("div", null);

P.S. Additionally, factory method configuration also affects JSX namespace lookup, for example default --jsxFactory React.createElement, first look up React.JSX, then look at global JSX namespace, if specify --jsxFactory h, first look up h.JSX

7. Summary

TypeScript's JSX type support is divided into element types, property types and result types 3 parts, as diagram below:

tsx

Reference Materials

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment