8
interface PageProps {
    foo?: Function;
    bar: number;
}

export class PageComponent extends React.Component<PageProps, {}> {
    public static defaultProps: Partial<PageProps> = {
        foo: () => alert('Did foo')
    };

    private doFoo() {
        this.props.foo(); // Typescript Error: Object is possibly 'undefined'
    }

    public render(): JSX.Element {
        return (
            <div>
                <span>Hello, world! The number is {this.props.bar}</span>
                <button onClick={() => this.doFoo()}>Do the Foo</button>
            </div>
        );
    }
}

Is there a way to tell Typescript that props.foo will always be defined?

There is a very good SO question and answer that discusses how to properly define the types for props on a component. It even discusses how you would let TS know about defaultProps on a stateless component.

However Typescript will still complain inside a regular component definition that your props might be undefined (as illustrated in my example above).

You can call it using a bang (this.props.foo!()) and that prevents TS from throwing an error, but it also prevents any kind of type checking on whatever you've passed in. In the example above, not a big deal. In more complex code, it has already bitten me.

So I am currently implementing things like this instead:

private doFoo() {
    if (this.props.foo) this.props.foo();
}

That has one drawback, which I think is rather important: It makes my code a little confusing. In reality, I always want to call this.props.foo(), but it seems like, without investigating the class more closely, there are cases where nothing will happen inside the doFoo method. (Yes I could add a comment explaining that the if is only for Typescript to be happy, but I'd rather my code speak for itself when it can.)

My code becomes even more unnecessarily complex when I actually need to return something from this.props.foo().

I am currently using @types/react for my type definitions.

Don
  • 746
  • 6
  • 18

1 Answers1

7

So, the compiler is saying that the object is possibly undefined. You know better that it's not, so you just want to write this.props.foo(). The right way to do it is using the null assertion type operator

this.props.foo!()

If you say that this has bit you before, then that's your own fault for not checking it :), which means the compiler was -right- that the object could have been undefined. And if there is a possibility it could be undefined, you should always check it. Perhaps you like a more shorter version of the if statement? It's one character less:

private doFoo() {
  this.props.foo && this.props.foo();
}

or with the typescript >= 3.8:

private doFoo() {
  this.props.foo?.();
}

On the other hand you can make an extra interface which extends the PageProps. For the fun of it, let's call it PagePropsFilled:

interface PagePropsFilled extends PageProps {
    foo: Function;
}

You can now set your this.props to PagePropsFilled, and it won't show you an error anymore

Poul Kruijt
  • 69,713
  • 12
  • 145
  • 149
  • You are my hero today, saved me from doing two interfaces for default props and props... <3 – AmazingTurtle Sep 01 '17 at 14:46
  • hmmm but how can the prop be `undefined` when we require type in `interface`? If you pass `undefined` Typescript should complain that it wants Function, if we don't pass anything, it should get the default value. This is confusing – apieceofbart Sep 14 '17 at 10:41
  • @apieceofbart If the interface property is written as `foo?: Function`, this means that any class implementing this interface has the choice to implement the `foo` method or not. If it does not implement it, it will return be `undefined`. In interfaces you cannot have a default value – Poul Kruijt Sep 14 '17 at 11:23
  • @PierreDuc thanks, I understand it now. My idea would probably require some changes to Typescript in order to work with react defaultProps. I still think it's a shame because we need to write checks (`if (this.props.foo)..` for the code that will always be true – apieceofbart Sep 26 '17 at 10:00
  • Well, read my given answer. If you are sure that it will always be true, you can use the `null assertion type operator`, like this: `this.props.foo!()` – Poul Kruijt Sep 26 '17 at 10:06
  • I'm sorry I didn't see the answer to this sooner and accept it. I cannot remember at the moment, but I think that my issue with using the ! was that doing so effectively turned the type into `any`. But I do not remember if that is what I meant. I didn't know TS *nearly* as well then as I do now. – Don Apr 01 '19 at 19:26