Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow different properties between the JSX expression and component type #55248

Open
5 tasks done
TheUnlocked opened this issue Aug 3, 2023 · 2 comments
Open
5 tasks done
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@TheUnlocked
Copy link

TheUnlocked commented Aug 3, 2023

Suggestion

πŸ” Search Terms

  • jsx functional component properties
  • property type
  • parameter type
  • jsx property cast

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Concept based on #14729

Introduce a new function to the JSX namespace called convertProperties (subject to change) which takes a component and returns an object representing its JSX props.

declare namespace JSX {
    // today's behavior
    function convertProperties<P>(component: (JSX.ElementClass & { new(): { props: P } }) | ((props: P) => JSX.Element)): P;

    // alternatively, if this were to supercede IntrinsicAttributes rather than just working alongside it
    function convertProperties<P>(component: (props: P) => JSX.Element): P & JSX.IntrinsicAttributes;
    function convertProperties<P, T extends { props: P }>(component: JSX.ElementClass & { new(): T }): P & JSX.IntrinsicAttributes & JSX.IntrinsicClassAttributes<T>;
}

When the typechecker sees a component expression (e.g. <MyComponent prop={val} />), it will determine the types of the required properties by performing a virtual call to JSX.convertProperties with the provided component type and using the return type. For example:

type SignalProps<P> = { [K in keyof P]: () => P[K] };

declare namespace JSX {
    function convertProperties<P>(component: ((props: SignalProps<P>) => JSX.Element)): P;
}

type MyProps = SignalProps<{
    title: string;
    count: number;
}>;

function MyComponent({ title, count }: MyProps) {
    return <div>{title()}: {count()}</div>;
}

function App() {
    // typeof MyComponent == (props: { title: () => string, count: () => number }) => JSX.Element
    // typeof JSX.convertProperties(MyComponent) == { title: string, count: number }
    return <MyComponent title="Hello, World!" count={3} />;
}

πŸ“ƒ Motivating Example

The Solid framework is very similar to React, but rather than tracking reactive dependencies with an explicit dependencies array like in React's useMemo and useEffect, Solid requires calling a function to retrieve the value of a piece of state, allowing for implicit dependency tracking based on which states were previously fetched the last time the memo/effect callback ran (see https://www.solidjs.com/guides/reactivity#introducing-primitives). It is a similar concept to Knockout's observables.

However, when it comes to component properties, Solid uses proxy dereferences instead of explicit function calls. This leads to a common footgun where destructuring the props object like one might do in React will prevent dependencies from being tracked (https://www.solidjs.com/guides/faq#why-do-i-lose-reactivity-when-i-destructure-props). An alternative design could have had the jsx factory function wrap every prop in a reactive getter before bundling them into the props object.

By separating the prop types between the component creation and the JSX where it's used, TypeScript would be able to support such a design in a new web framework.

πŸ’» Use Cases

See above.

Another similar use case could be:

type DetailedProps<P> = {
    [K in keyof P]: {
        value: P[K];
        prevValue?: P[K];
    }
}

declare namespace JSX {
    function convertProperties<P>(component: ((props: DetailedProps<P>) => JSX.Element)): P;
}

This feature would also allow performing the reverse operation of JSX.IntrinsicAttributes. For example:

declare namespace JSX {
    function convertProperties<P>(component: ((props: P) => JSX.Element)): Omit<P, 'parent'>;
}

type PropsWithParent<T> = T & {
    parent?: JSX.Element;
};

function MyComponent({ parent }: PropsWithParent<{}>) {
    // ...
}

function App() {
    return <MyComponent />;
}

Alternative Designs

Alternatively, a solution which is slightly less powerful but likely just as useful in 90% of cases could be to declare generic types on the JSX namespace like is done with JSX.IntrinsicClassAttributes<T>:

declare namespace JSX {
    // same behavior as today
    type TransformProps<P> = P;

    // some use cases
    type TransformProps<P> = { [K in keyof P]: P[K] extends () => infer R ? R : never };
    type TransformProps<P> = Omit<P, 'parent'>;

    // or in reverse, deriving JSX prop types via inference like in the earlier examples
    type TransformProps<P> = SignalProps<P>;
    type TransformProps<P> = P & { parent?: JSX.Element };
}
@RyanCavanaugh
Copy link
Member

cc @weswigham

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Aug 10, 2023
@robbiespeed
Copy link

It seems like #14729 could cover this use case as well as others (Playground).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants