简体   繁体   中英

React and Typescript Interface “OR” operator

I am starting to develop an application with React and Typescript. I am newbie with the 2 things by the way.

I have a react component and some interface, I want the every field in that interface is required, but if one specific field is implemented the others are not required anymore.

In this case if I set "object" I would like to not need implement the other 2 fields.

interface ITextFieldProps {
    label: string,
    required: boolean,
    object?: any
}

export default class TextField extends React.Component<ITextFieldProps, any> {
    ...
}

Hope someone can help, cheers!

EDIT

First of all I want to thank all the answers, I really learned a lot with each one of them. They helped me to understand my problem and to find the best solution.

The object that I will pass is a metadata so it will be only available in runtime after a http call. That being said, I cannot check and type it at dev time anyway.

So I solved my problem using the react props spread.

(Today it is not supported by typescript, but someday it will be someday hehe https://github.com/Microsoft/TypeScript/issues/2103 ):

export interface IMetadata {
    label: string,
    required: boolean
}

interface TextFieldProps extends IMetadata {
    ...
}

This way i can pass one object with the props spread and then when i call the label="" it will overwrite the label comming in {...this.metadata.nome} automatically:

<TextField {...this.metadata.nome} label="OverwriteLabel" />

EDIT 2

And as Daniel said: Unfortunately the type checking in JSX has a bug, see:

https://github.com/Microsoft/TypeScript/issues/10171

They plan to fix it for 2.1. Until then, this technique won't work for TSX. (When it is fixed, I will come back and update this answer.)

This is done in TypeScript with a union type.

interface TextFieldPropsA {
    label: string,
    required: boolean
}

interface TextFieldPropsB {
    object: any
}

Here's how you can define a union of those:

type TextFieldProps = TextFieldPropsA | TextFieldPropsB;

This literally means it must be the first or the second type (it's exactly what you asked for in your question! We're "or"-ing two interfaces).

So:

// no good
const test1: TextFieldProps = { }

// no good, still missing the label
const test2: TextFieldProps = {
    required: true
} 

// this is fine
const test1: TextFieldProps = {
    object: {}
}

(By the way, note that the I prefix is not widely used in TypeScript, because classes and function types are all interfaces so it really doesn't tell you anything useful.)

UPDATE However, you require this to define React props, so you can restrict what is allowed when using JSX syntax. Unfortunately the type checking in JSX has a bug, see:

https://github.com/Microsoft/TypeScript/issues/10171

They plan to fix it for 2.1. Until then, this technique won't work for TSX. (When it's fixed I'll come back and update this answer!)

You can't do that, there's no way to express in an interface that if one property is satisfied then the other aren't needed.

You have two options (as I see it):

(1) Declare all properties as optional:

interface ITextFieldProps {
    label?: string,
    required?: boolean,
    object?: any
}

And then check if the value is valid:

function isValid(props: ITextFieldProps): boolean {
    return props.label || props.required != null || props.object;
}

(2) Define a few interfaces which extend a base one:

interface ITextFieldPropsBase {}

interface ITextFieldPropsLabel extends ITextFieldPropsBase {
    label: string;
}

interface ITextFieldPropsBool extends ITextFieldPropsBase {
    value: boolean;
}

interface ITextFieldPropsAny extends ITextFieldPropsBase {
    object: any;
}

class TextField<T extends ITextFieldPropsBase> extends React.Component<T, any> { ... }

This way you only get one of those properties each time.


Edit

@DanielEarwicker commented about how do props are being passed into the component in my 2nd example.
When using jsx :

ReactDOM.render(<TextField label="hey" />, document.getElementById("container"));

It errors that label cannot be found, but it can be done using ReactDOM.render :

ReactDOM.render(React.createElement(TextField, { label: "hey" }), document.getElementById("container"));

2nd Edit

It seems that my 2nd suggestion isn't a good one for this specific case.
I derived it from a slightly different scenario that I have:

interface BaseProps {
    name: string;
}

abstract class BaseComponent<T extends BaseProps> extends React.Component<T, {}> {
    render() {
        return <div className={ this.props.name }>{ this.getContent() }</div>
    }

    protected abstract getContent(): JSX.Element;
}

interface DateProps extends BaseProps {
    date: Date;
}

class DateComponent extends BaseComponent<DateProps> {
    protected getContent(): JSX.Element {
        return <div>{ DateComponent.formatDate(this.props.date) }</div>
    }

    private static formatDate(date: Date): string {
        // ...
    }
}

As I actually have a different component class for each of the interfaces then I have no need to do any type guards, casting or any other tricks.

The answer by @DanielEarwicker is the right one in the scenario described by the OP.

It sounds like the minimal interface for this component is always:

interface ITextFieldProps {
    label: string;
    required: boolean;
}

But sometimes you have an object with more properties than just those two.

If that's the case, then you can just pass the more complex object as props to the component. As long as it has those two properties defined, it's OK.


In response to your comments, I'd warn against making the props too flexible - it comes at the cost of making the component unnecessarily complex. I'd suggest always passing named individual props rather than using a more general object prop.

I guess that you can use something like this:

// use the object
const { label, required } = object;

// ...or use a mixture
const { label } = object;
const required = false;

// ...or define everything manually
const label = "label";
const required = true;

return <TextField label={label} required={required} />;

This way, the last line remains the same in all cases, so the props interface doesn't need to do anything fancy.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM