Typescript: Using lookahead to detect generic JSX elements?

Created on 8 Jan 2016  Â·  32Comments  Â·  Source: microsoft/TypeScript

_Initially from a couple different comments, but elaborated here._

Currently, in order to use a generic JSX element, you must alias both the type and interface to a non-generic specialization.

class Select<T> extends React.Component<SelectProps<T>, any> {}

// Alias
type StringSelect = new () => Select<string>
const StringSelect = <StringSelect> Select

const Form = () => <StringSelect items={['a','b']} />

It would be a _lot_ more convenient and consistent to use something like this:

const Form = () => <Select<string> items={['a', 'b']} />

In order to differentiate the latter, you can use a single token of lookahead in the parser to know if it's a generic JSX element:

  • If the next token is a <, like in < Select <, then consume the rest of the type as a generic type. After consuming that:

    • If the next token is a >, then finish the type as a generic type cast.

    • If the next token is an identifier or a { (for spread attributes, when they get implemented), then parse the rest as a JSX element.

    • Otherwise, parse the rest as a type cast.

  • If the next token is a >, like in < Select >, then finish the type as a simple type cast.
  • If the next token is an identifier or a {, then parse the rest as a JSX element.
  • Otherwise, consume the rest as a type cast.

I don't know of any reason that wouldn't be feasible to do, considering async arrow functions, which are already implemented, require potentially a whole call expression of parsing to differentiate between let f = async (x, y), which is a call expression, and let f = async (x, y) => x, which is an async arrow function. (I feel sorry for whoever ends up implementing that in V8, which doesn't support any backtracking or much lookahead for memory reasons.)

/cc @RyanCavanaugh

JSTSX Difficult Fixed Suggestion help wanted

Most helpful comment

We'll be basically redoing JSX typechecking in 2.1 or 2.2 and can probably look at this at that time.

All 32 comments

We've gotten this request a number of times and it does seem reasonable in terms of parsing.

Accepting PRs.

This should be easy to parse but I think it's going to be some rather difficult work in the checker.

@RyanCavanaugh whats status of this issue?
It's very impotant functionality.


import * as React from 'react';

interface MyTestProps<T> {
    values: Array<T>;
    selectHandler: (selectedVal: T) => void;
}

export class MyTest<T> extends React.Component<MyTestProps<T>, {}> {

    render() {
        return (
            <ul>
                { this.props.values.map(val =>
                    <li>{val}</li>
                ) }
            </ul>
        );
    }
}

class TestMyTest extends React.Component<{}, {}> {
    render() {
        return (
            <MyTest values={[1, 2, 3, 4]} selectHandler={(val) => { } } />
        );
    }
}

compiler throws error "type number[] is not assignable to type T[]"

But in this example


import * as React from 'react';

interface MyTestProps<T> {
    values: Array<T>;
    selectHandler: (selectedVal: T) => void;
}

export class MyTest<T> extends React.Component<MyTestProps<T>, {}> {

    render() {
        return (
            <ul>
                { this.props.values.map(val =>
                    <li>{val}</li>
                ) }
            </ul>
        );
    }
}

new MyTest({ values: ['a', 'b'], selectHandler: (v) => { v.length } });

selectHandler param v have type string;

@RyanCavanaugh Could this be part of a future planned milestone? 2.1 or 2.2?

Maybe a nicer API as workaround: we introduce a static function of on components with generics:

import React, { Component } from 'react';
interface FooProps<T> {
  data: T;
  someCallback: (data: T) => void;
}

class Foo<T> extends Component<FooProps<T>, void> {
  // `of` is created here
  static of<T>() {
    return Foo as new () => Foo<T>;
  }

  render() {
    // make something with this.props.data here...
    return (
      <div onClick={() => this.props.someCallback(this.props.data)}>
        Click me!
      </div>
    );
  }
}

interface CoolData {
  baz: string;
}

const coolData: CoolData = {
  baz: 'Hello'
};

// `of` is used here
const FooWithCoolData = Foo.of<CoolData>();

const Bar = () =>
  <FooWithCoolData
    data={coolData}
    someCallback={data => console.log('I know data is CoolData here!')}
  />;

Or if we don't want to use <T> syntax, but use the actual object, we do this (I'm still surprised this works):

// new `of` inside `class Foo<T>`
  static of<T>(t: T) {
    return Foo as new () => Foo<T>;
  }

// new usage of `of`
const FooWithCoolData = Foo.of(coolData);

We'll be basically redoing JSX typechecking in 2.1 or 2.2 and can probably look at this at that time.

Cool. Thank you for the update! :)

Any update ?

@RyanCavanaugh Please consider supporting at least type inference when possible. No need to add new syntax. Just having a generic component infer its type parameters solve a huge number use-cases.

@alitaheri I believe the existing JSX work going on at #12107 will enable that scenario

Still not working on [email protected].
The typescript team is making working with typescript and react a very fantastic experience, but unfortunately this issue is one of the most annoying that still remains unsolved.

The workaround works fine (

const BooleanSelect: new () => SingleColumnSelect<boolean | null> = SingleColumnSelect;
  <BooleanSelect values={[null, true, false]}/>

) but it's not only ugly and un-elegant, it is also really unclear for newcomers to react&typescript

Either solution (allowing to specify the generic inside jsx or using type inference) would be really really welcome

@MastroLindus in the meantime you might consider this workaround, which I find to be a bit simpler to read and write:

class Select<T> extends React.Component<...> {...}
class BooleanSelect extends Select<boolean> {}
class SomeComponent extends React.Component<{}, {}> {
  render() {
    return <BooleanSelect />; // this works fine
  }
}

@zzzev I am aware of it( or others, like just a simple cast) but to define a new class just for the workaround somehow it feels even dirtier to me, even if in practice the two forms are not that much different... I simply cannot find one that I am really satisfied about

We are still need a way to infer type parameter in the original example for generic React class component but here are some possible work around in some cases:

(Note this is for 2.3 compiler)
* Using SFC *
as mentioned by @RyanCavanaugh's comment, compiler now can infer type parameter in SFC

import * as React from "react";

type SelectProps<T > = { items: T[] }
function Select<T>(attr: SelectProps<T>) {...}

const someItems = [1, 2, 3, 4];
const someItems2 = ["ss"];

function renderJsx() {
    return <Select items={someItems} />; // Ok
}

Nightly & future 2.3.3
Using default type parameter (in this example, it may not be desirable to do so but the compiler will not give you error for being generic)

import * as React from "react";

type SelectProps<T > = { items: T[] }
class Select<T = any> extends React.Component<SelectProps<T>, any> { }

const someItems = [1, 2, 3, 4];
const someItems2 = ["ss"];

function renderJsx() {
    return <Select items={someItems} />; // OK
}

Would love to have this feature - would enable a lot of good scenarios :)

Is this likely to ever be implemented?

Would also like to ping this topic.

I'm implementing a smoother way of handling forms in react and it really depends on having generic types. The workaround brings back much of the boilerplate I'd like to avoid :/

what I've noticed:

import * as React from 'react';

interface Props<T> {
  a: T
  b: T[]
}

function Comp1<T>(props: Props<T>) {
  return null
}


class Comp2<T> extends React.Component<Props<T>> {
  render(){
    return null
  }
}

const props = {
  a: '',
  b: [2],
}

const a = <Comp1 {...props} /> //error here

React.createElement(Comp1, props)

const b = <Comp2 {...props} />

React.createElement(Comp1, props)

So, createElement every time passing T=any.
When using jsx and component is Stateless Component - checking works. So, workaround for now could be to wrap your generic components with Stateless comp.
//oh, looks like you already know this, sorry

Still as annoying as ever :D

So it's more than a year now since the issue was opened, we are going for a Typescript 2.8 and is not even on the roadmap? I don't get it...

maybe not so trivial to do

@antanas-arvasevicius Apparently, the checker is the difficult part. If you read my initial bug, it was me addressing the other implementation aspect, mentioned in comments linked to from there.

This should in theory become easier (possibly even trivial) once @weswigham 's work to refactor JSX typechecking is complete.

I checked, and idle complaining that we haven't implemented popular a feature request has not caused new engineer time to appear.

We actually do inference and generic instantiation for generic JSX components as of our last release, so we don't even actually really _need_ anything more than the parse step for this anymore (I mean, ofc the call resolution need to be modified to check for type arguments before doing inference, but still - more done than before).

Also, the OP is a tad out of date; Unless I'm mistaken, we don't even support parsing angle bracket style typecasts in JSX files, so the described ambiguity wouldn't exist.

<T>e expressions aren't supported in JSX. The only exception to <T> in a leading expression position starting a JSX element is a generic arrow function <T>(x: T) => e, which I believe has some limited lookahead to try to disambiguate (or maybe we required you to write T extends {} ? I'd have to check).

We require a constraint on the first type parameter.

I stand corrected, idle complaining actually did improve the situation 😆

:D how about other issues related to TSX? Especially deprecating magic in
global JSX namespace and instead using type inference directly from
createElement? Allowing multiple JSX definitions would allow to use not
only react, but create many html like DSL's for e.g. database schema
definitions, permission roles, state machines which "config like
structures" could be directly translated to js.

On Fri, 9 Mar 2018 at 01:05, Ryan Cavanaugh notifications@github.com
wrote:

I stand corrected, idle complaining actually did improve the situation 😆

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/Microsoft/TypeScript/issues/6395#issuecomment-371655555,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AEAcnO_LOm60bVGqc6cTk5FYGTTvR85zks5tcbk2gaJpZM4HBBqf
.

@antanas-arvasevicius We've merged code that allows us to look up the current global types in a namespace local to the factory function instead of globally already, which should be a significant improvement once libraries move to utilizing it. (And combined with the also recently introduced @jsx pragma will allow you to use multiple jsx variants in a project; just only one per-file)

We _want_ to move to just using the factory function's signature, but there's a slight problem - to create a perfectly well-typed factory function that we can actually do inferences for, you need ReturnType (and InstanceType and FirstArgument) conditional type-types that are overload-aware (otherwise you need compiler internal magic that assumes how the factory function works or looks things up in a namespace, like we do now). As is, we could basically do it, but we'd break overloading on stateless function components (we'd always pick the last signature), which would be no bueno; so fully moving away from the JSX namespace (anywhere) might require that we either improve conditional types or implement real "call" types first. We're actively looking at it, though!

Thank you for review of what's going on, it is more complicated/interesting
than it looked :) One year ago I had a trouble with JSX that I couldn't
declare that ElementType would be dependable on InstanceType e.g.
would return real instance of object which is list and not an ADT like
structure as react vdom because not all frameworks works in vdom mode
extjs, isomorphic smartclient etc, but their GUI could be represented in
jsx way, my opened issue
https://github.com/Microsoft/TypeScript/issues/13890
Please consider it also, and if factory function is difficult/impossible to
implement maybe that proposal for ElementType could be released?

On Fri, 9 Mar 2018 at 01:36, Wesley Wigham notifications@github.com wrote:

@antanas-arvasevicius https://github.com/antanas-arvasevicius We've
merged code that allows us to look up the current global types in a
namespace local to the factory function instead of globally
https://github.com/Microsoft/TypeScript/pull/22207 already, which
should be a significant improvement once libraries move to utilizing it.

We want to move to just using the factory function's signature, but
there's a slight problem - to create a perfectly well-typed factory
function that we can actually do inferences for, you need ReturnType (and
InstanceType and FirstArgument) conditional type-types that are
overload-aware (otherwise you need compiler internal magic that assumes how
the factory function works or looks things up in a namespace, like we do
now). As is, we could basically do it, but we'd break overloading on
stateless function components (we'd always pick the last signature), which
would be no bueno; so fully moving away from the JSX namespace (anywhere)
might require that we either improve conditional types or implement real
"call" types first. We're actively looking at it, though!

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/Microsoft/TypeScript/issues/6395#issuecomment-371662348,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AEAcnNcLWtsZ97TrJo3wowHEyXSiZzpRks5tccBngaJpZM4HBBqf
.

Since this is not compatible with the JSX spec as it is right now, it would’ve been good to have the discussion over on the JSX spec repo.

JSX already allows for nested elements in the attribute position <Foo foo=<Foo /> /> so it’s possible this wouldn’t play well with other additions. I don’t see an immediate problem but would be good to take a pass over there in the future like we agreed to do for other additions.

I don’t see an immediate problem but would be good to take a pass over there in the future like we agreed to do for other additions.

Apologies - we've had this issue open for over two years with a pretty straightforward design desired from the beginning (implementation of typechecking being the real blocker, which we quietly implemented without the syntax in 2.7); I wasn't aware we had (or needed, since it's a type-based syntax addition, like type arguments for call or new expressions) any renewed outreach set up around this (probably something to do with me not having worked on it 3 years ago XD).

JSX already allows for nested elements in the attribute position <Foo foo=<Foo /> />

As an aside, occasionally I've heard people familiar with the jsx spec point that out (within the team, I think it's a favorite "I bet you didn't know x was valid jsx" fact). We still don't parse it - it's been years since we first noticed it and _still_ haven't gotten a serious issue for it. Still seems underused. It _is_ still broken in babel 6. (fixed in the 7 beta though, so it's got that going for it, though fragments are broken right now) Given how it was spoken of the last time it came up, its future isn't exactly golden (FYI, babylon7 is the only parser on ASTExplorer that can parse the fragment production, but babel can't actually transpile it right now. heh.).

But it was brought up while I implemented this, IIRC 😉 We don't think there are any obvious conflicts (beyond the ones we already have to disambiguate with leading generic parameters and occasionally left shift token that occur elsewhere), either (unless someone's going to start suggesting silly things like jsx elements as property names in fragments or something). If you're looking for a brief on how it was implemented, in our case it's just, after the JSXElementName production in opening/self closing tags, (optionally) a one-token lookahead for <, followed by our TypeArgumentList production (I'm not sure how you'd try to represent leaving space for such a thing in the JSX spec) and a required closing > (before continuing with standard JSX production).

Was this page helpful?
0 / 5 - 0 ratings

Related issues

bgrieder picture bgrieder  Â·  3Comments

manekinekko picture manekinekko  Â·  3Comments

DanielRosenwasser picture DanielRosenwasser  Â·  3Comments

dlaberge picture dlaberge  Â·  3Comments

wmaurer picture wmaurer  Â·  3Comments