Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Better support for 'defaultProps' in JSX #23812

Copy link
Copy link
Closed
@DanielRosenwasser

Description

@DanielRosenwasser
Issue body actions

This proposal aims to fix problems around React's use of defaultProps, which TypeScript has trouble understanding today.

Background

Today, React has a concept called defaultProps; these are props that an external user of a component may consider optional, but are always present in the component definition.

export class Greeting extends React.Component {
  static defaultProps = {
    name: 'stranger'
  }

  render() {
    return (
      <div>Hello, {this.props.name}</div>
    )
  }
}

let myElement1 = <Greeting name="Daniel!"/>; // <div>Hello, Daniel!</div>
let myElement2 = <Greeting />;               // <div>Hello, stranger</div>

Unfortunately, TypeScript --strictNullChecks users who want to take advantage of this are in a tough spot.
They can either specify these props as optional and assert that defaulted properties are present internally,

export interface GreetingProps {
  name?: string
}

export class Greeting extends React.Component<GreetingProps> {
  static defaultProps = {
    name: 'stranger'
  }

  render() {
    // Notice the non-null assertion!
    //                           | |
    //                           | |
    //                           \ /
    //                            V
    return (
      <div>Hello, {this.props.name!.toUpperCase()}</div>
    )
  }
}

or they can jump through levels of indirection:

export interface GreetingProps {
  name?: string
}

// Notice we are using a 'const' declaration here!
export const Greeting: React.Component<GreetingProps> = class extends React.Component<GreetingProps> {
  static defaultProps = {
    name: 'stranger'
  }

  render() {
    return (
      <div>Hello, {this.props.name!.toUpperCase()}</div>
    )
  }
}

Proposals

Using resolution semantics of the JSX factory

Users can already emulate this behavior with React.createElement: https://tinyurl.com/y9jbptt9

// React-like API

export interface Component<P> {
    props: P;
}

export interface ComponentClass<P, Defaults = void> {
    new(props: P): Component<P>;
    defaultProps: Defaults;
}

export interface Element<P> {
    type: string | ComponentClass<P>;
    props: P;
}

// Some additions

type Defaultize<T, Defaults> =
    & Partial<Pick<T, Extract<keyof T, keyof Defaults>>>
    & Pick<T, Exclude<keyof T, keyof Defaults>>;

declare function createElement<Props, Defaults>(
    type: ComponentClass<Props, Defaults>,
    props: Defaultize<Props, Defaults> | null):
    Element<Props>

// Now comes your code...

class FooComponent {
    props: { hello: string, world: number };
    static defaultProps: { hello: "hello" };
}

// works: 'hello' got defaulted
createElement(FooComponent, { world: 100 });

// error: missing 'world'
createElement(FooComponent, { hello: "hello" });

In theory, just passing the right data to the JSX factory should do the work for us.

However, there are some issues:

  1. The Defaultize type is hard to read.
  2. TypeScript treats the object literal in createElement as an inference site, and when inference fails, users get no completions for props. It's not ideal in the general case, but the goal is to continue to give a good experience in JSX.

Add a new type in the JSX namespace

We could potentially add a new field called ComponentDefaultPropNames in JSX which would either be a union of literal types or possibly an object type whose property names describe the respective names that defaultProps could have:

namespace JSX {
    export type ComponentDefaultPropNames = "defaultProps";
}

When writing out a JSX tag for a component, JSX.ComponentDefaultPropNames is looked up on the type of the tag name to determine which properties should be considered optional.

So for example:

<TagName {...someObject}>

If TagName expects props with the type Props, TypeScript can relate typeof someObject to some combination of Props and (typeof TagName)[JSX.ComponentDefaultPropNames].

Potential issues

Under either proposal, some type is inferred from the type of the component's defaultProps property.
Since React's .d.ts files currently define defaultProps to be a Partial<P>, where P is the type parameter for the type of props, we need to avoid that from making all props optional.

To avoid issues here, we could say that only required properties on defaultProps satisfy a JSX element's type. So for example:

interface MyProps {
  name: string;
  age: number;
}

class MyComponent extends React.Component<MyProps> {
  render() {
    return <div>My name is {this.props.name} and I am {this.props.age} years old.</div>;
  }

  static defaultProps: { name?: string, age: number } = {
    name: "Daniel",
    age: 26,
  }
}

// error: 'name' is still considered required
let x = <MyComponent />

// works! 'age' is optional because it was definitely present in the type of 'defaultProps'.
let y = <MyComponent name="Daniel" />

Metadata

Metadata

Assignees

Labels

Domain: JSX/TSXRelates to the JSX parser and emitterRelates to the JSX parser and emitterFixedA PR has been merged for this issueA PR has been merged for this issueSuggestionAn idea for TypeScriptAn idea for TypeScript

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions

    Morty Proxy This is a proxified and sanitized view of the page, visit original site.