Typescript: TS files cannot use JSX components declared as a classes in JS/JSX file

Created on 9 Mar 2017  路  38Comments  路  Source: microsoft/TypeScript

CC @donataswix

It appears that using a React component written as a class in a .js file causes some issues.

Start a project with the following files:

tsconfig.json

{
  "compilerOptions": {
    "module": "es2015",
    "target": "esnext",
    "jsx": "react-native",
    "strictNullChecks": true,
    "allowSyntheticDefaultImports": true,
    "allowJs": true,
    "lib": [
      "es5",
      "es6",
      "es7",
      "es2017",
      "dom"
    ],
    "outDir": "dist"
  },
  "include": [
    "**/*.ts",
    "**/*.tsx"
  ],
  "exclude": [
    "node_modules"
  ]
}

index.tsx

import React from 'react';
import Hello from './hello';

class A extends React.Component<void,void> {
  render() {
    return (
      <div>
        <Hello/>
      </div>
    );
  }
}

hello.js

import React from 'react';

export default class Hello extends React.Component {
  render() {
    return <div>Hello</div>;
  }
}

Expected: No problems
Actual: Error on <Hello />: JSX element type 'Hello' does not have any construct or call signatures.

Note that if you add type arguments to Component in hello.js, this will actually fix the problem (while creating new ones).

Potentially related is #13609.

Bug Fixed

Most helpful comment

Helps for me

//this is tsx file

import React, { Component } from "react";
import SomeComponent from './SomeComponent.js';

/* tslint:disable */
const JsSomeComponent: any = SomeComponent; //todo delete me after refactoring to TS
/* tslint:enable */

class MyTSXComponent extends Component {
    public render() {
        return <JsSomeComponent/>;
    }
}

All 38 comments

Can you give https://github.com/Microsoft/TypeScript/pull/14396 a try and see if it fixes the issue?

This is killing me trying to migrate a large project to Typescript. I've got a hundred plain ol React components and I'd love to be able to do 1 at a time, but once converted to .tsx, they can't import any other .js files without this error.

This is addressed by https://github.com/Microsoft/TypeScript/pull/14907. @timothyallan please give [email protected] a try and let us know if you are running into other issues.

This is not solved with [email protected] I am still seeing issues

I see it working on latest. if you are seeing issues please file a new issue:

c:\test\14558>type tsconfig.json
{
  "compilerOptions": {
    "module": "es2015",
    "target": "esnext",
    "jsx": "react-native",
    "strictNullChecks": true,
    "allowSyntheticDefaultImports": true,
    "allowJs": true,
    "checkJs": true,
    "lib": [
      "es5",
      "es6",
      "es7",
      "es2017",
      "dom"
    ],
    "outDir": "dist"
  },
  "include": [
    "**/*.ts",
    "**/*.tsx",
    "**/*.js"
  ],
  "exclude": [
    "dist",
    "node_modules"
  ]
}
c:\test\14558>type index.tsx
import React from 'react';
import Hello from './hello';

class A extends React.Component<void, void> {
  render() {
    return (
      <div>
        <Hello>
          <div></div>
        </Hello>
      </div>
    );
  }
}
c:\test\14558>type hello.js
// @ts-check

import React from 'react';

export default class Hello extends React.Component {
  render() {
    return <div>Hello</div>;
  }
}

c:\test\14558>tsc --v
Version 2.3.2

c:\test\14558>tsc --p tsconfig.json

c:\test\14558>echo %ERRORLEVEL%
0

does it work if you rename hello.js to hello.jsx ?

c:\test\14558>move hello.js hello.jsx
        1 file(s) moved.

c:\test\14558>
tsc

c:\test\14558>echo %ERRORLEVEL%
0

@mhegazy

Did you try adding properties to Hello?
It seems like Typescript infers that properties of Hello are of type {}. So you cannot render it from TypeScript as <Hello baz="world"/>.

Here is the compiler output:

error TS2339: Property 'baz' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<Hello> & Readonly<{ children?: ReactNode; }> & Readonly<{}>'

It can be worked around with React.createElement:

React.createElement(Hello, {baz: "world"} as any)

or by creating temporary variable with properties but this is quite ugly workaround. Can this be done better?

https://github.com/gagarski/tsx-jsx - complete minimalistic example

Can we please reopen it? It doesn't work if component have any props inside and it's a show stopper for React projects which are migrating to TypeScript

@mhegazy this issue has not been resolved. Is there anyone looking at this?

Agree, it's not resolved. I too am facing the same issue and would like a resolution.

+1 I have the same problem guys

The original issue was fixed. but then a change was made to the react.d.ts to make so the default props is {}, i.e. in a .js file all JSX component have no properties.

this is the same issue reported in https://github.com/Microsoft/TypeScript/issues/18134..

the fix is to change the react declaration file to be P = object instead of P = {} to allow for accessing props.

@mhegazy

I tried to monkey-patch @types/react in my example here (npm i and then just editing node_modules/@types/react/index.d.ts)

Here is patched React.Component definition:

interface Component<P = object, S = object> extends ComponentLifecycle<P, S> { }

But I still get a compilation error for this example:

index.tsx(8,16): error TS2339: Property 'baz' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<Hello> & Readonly<{ children?: ReactNode; }> & object'.

However, when I am using /** @augments {React.Component<object, object>} */ on JSX class, everything works fine (without monkey-patching).

you are right. we need to remove the default or make it any. and in both cases, seems a break.

@weswigham can you take a look here. unfortunately we can not remove the defaults from react definition file as that would be a big breaking change, but what can we do not make sure this scenario still works.

Yeah, removing the default would broke a lot of code and any is seems wrong here (component with no state specified should be stateless, not have any state, the same logic should be applied for props).

sadly also suffering from this issue -- is a huge deal when trying to migrate incrementally to typescript.

@mhegazy any update here? I'm in a similar boat to @z0d14c in which this makes adopting ts in a large codebase very difficult.

This worked for me
import * as React from 'react' instead of import React from 'react'

I'm bumping into this as well. For now, thinking the only sane way to migrate to Typescript is to start with components that don't import any other jsx components and work my way to the top.

So we know the root cause of this issue - React's type definitions have their default Props and State types set to {} - the intention being that this gives you safety; if you specify the generics, then you can access props and state safely, otherwise you get no props or state (other than what you may inherit from intrinsics). The downside to this is that in JS, you often _don't_ or _can't_ specify the generic arguments - so they default to {} (whereas previous they defaulted to any), and you can't do anything with the props or state without an error. In short, React's types used to default open, but now default closed. This behavior is arguably better for TS users, but is without a doubt a showstopper for JS users relying gradual typing with the same types. We're trying to come up with a solution that can satisfy both user-bases, but it's difficult.

While we work on it, a workaround in the meantime is editing the react.d.ts to use any for its' default props and state types (the replacement would be this: interface Component<P = any, S = any> extends ComponentLifecycle<P, S> { }), _or_ adding JSDoc arguments to your JS components to specify their props and/or state types (which you should consider preferred if it is easy, as when you migrate the file to TS we provide a bunch of quick fixes to translate that JSDoc into proper TS code), for example:

import React, { Component } from "react";

/**
 * @augments {Component<{location: string}, *>}
 */
class MyComponent extends Component {
    render() {
        return <h1>Hello World from {this.props.location}</h1>
    }
}

const x = <MyComponent location="Redmond" />

Helps for me

//this is tsx file

import React, { Component } from "react";
import SomeComponent from './SomeComponent.js';

/* tslint:disable */
const JsSomeComponent: any = SomeComponent; //todo delete me after refactoring to TS
/* tslint:enable */

class MyTSXComponent extends Component {
    public render() {
        return <JsSomeComponent/>;
    }
}

We have a fix on the compiler side in https://github.com/Microsoft/TypeScript/pull/19977 to accommodate the react changes. Can you give tomorrow's typescript@next a try?

Tempted to try new fix. For the time being or for everyone stuck on old versions in future:

  • use @augments as seen in this thread for stateful components. Like this:
/**
 * @augments {React.Component<*, *>}
 */
  • use @return for stateless components and return "any". I tried returning React.SFC<*> but that didn't work. No idea what I'm doing. Returning any works fine. Like this:
/**
 * @return {*}
 */

@mhegazy this fix does not work if noImplicitAny compiler flag is enabled :(

Well.. it is an implicit any... if you want the strict no-anys-implicitly-added-by-the-compiler, then use `/** @augments */ to specify the type.

@mhegazy so in noImplicitAny-mode /** @augments ... */ is the only solution and it will not be changed in the future?

@mhegazy This makes it very difficult to incrementally adopt TS in a large codebase because we have hundreds/thousands of JS components that will not be converted for the foreseeable future.

but it is an implicit any. you can opt-out of this using --noImplicitAny false

so i miss understood the @vkrol's comment. I thought he meant getting an error under --noImpclitAny --checkJs which is expected.. but looking at the code again, it is disabled for --noImplcitAny, and that is a bug and should be fixed. apologies for the miss understanding.

Just ran into this as well, just so I'm understanding, with #20232 this fix should work for an incrementally converted codebase when using a non TS React component inside of a TSX component with noImplicitAny enabled

@mhegazy @weswigham it is works in the latest nightly build. Thanks!

I ran into this as well.. while using react-select with latest typings. Has this issue been fixed in TS 2.6.2?

@danantal Running into this as well. Not on TS 2.6.2, though. I would upgrade, but it's a massive jump for the codebase.

This works for me:

//this is tsx file

import React, { Component } from "react";

const SomeComponent = require('./SomeComponent.js');

/* tslint:disable */
const JsSomeComponent: any = SomeComponent; //todo delete me after refactoring to TS
/* tslint:enable */

class MyTSXComponent extends Component {
    public render() {
        return <JsSomeComponent/>;
    }
}
Was this page helpful?
0 / 5 - 0 ratings