Home > Enterprise >  Element implicitly has an 'any' type because expression of type 'string' can
Element implicitly has an 'any' type because expression of type 'string' can

Time:11-22

I was following TypeScript example provided by Guilherme on this thread (pasted bellow).
But I'm getting an error: Element implicitly has an 'any' type because expression of type 'string | number | symbol' can't be used to index type 'IState'. No index signature with a parameter of type 'string' was found on type 'IState'.ts(7053)
Following this article I've tried to add:

    const result: IState = { ...state };
    result[action.type as keyof IState] = action.value;
    return result;

but this doesn't solve the issue. There are many responses to such question, but I'm missing something.

import React, { FC, Reducer, useReducer } from "react";

interface IState {
    email: string;
    password: string;
    passwordConfirmation: string;
    username: string;
}

interface IAction {
    type: string;
    value?: string;
}

const initialState: IState = {
    email: "",
    password: "",
    passwordConfirmation: "",
    username: "",
};

const reducer = (state: IState, action: IAction) => {
    if (action.type === "reset") {
        return initialState;
    }

    const result: IState = { ...state };
    result[action.type] = action.value;
    return result;
};

export const Signup: FC = props => {
    const [state, dispatch] = useReducer<Reducer<IState, IAction>, IState>(reducer, initialState, () => initialState);
    const { username, email, password, passwordConfirmation } = state;

    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();

        /* fetch api */

        /* clear state */
        dispatch({ type: "reset" });
    };

    const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const { name, value } = e.target;
        dispatch({ type: name, value });
    };

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label>
                    Username:
                    <input value={username} name="username" onChange={onChange} />
                </label>
            </div>
            <div>
                <label>
                    Email:
                    <input value={email} name="email" onChange={onChange} />
                </label>
            </div>
            <div>
                <label>
                    Password:
                    <input
                        value={password}
                        name="password"
                        type="password"
                        onChange={onChange}
                    />
                </label>
            </div>
            <div>
                <label>
                    Confirm Password:
                    <input
                        value={passwordConfirmation}
                        name="passwordConfirmation"
                        type="password"
                        onChange={onChange}
                    />
                </label>
            </div>
            <button>Submit</button>
        </form>
    );
};

CodePudding user response:

The main cause for that error can be addressed by changing the definition of IAction to:

interface IAction {
    type: keyof IState;
    value?: string;
}

which uses the keyof type operator to ensure that the type property can only be one of the keys of IState.

With this you'll have two other errors though (one of which may have been immediately apparent):

main.ts:21:9 - error TS2367: This condition will always return 'false' since the types 'keyof IState' and '"reset"' have no overlap.

21     if (action.type === "reset") {
           ~~~~~~~~~~~~~~~~~~~~~~~

main.ts:26:5 - error TS2322: Type 'string | undefined' is not assignable to type 'string'.
  Type 'undefined' is not assignable to type 'string'.

26     result[action.type] = action.value;
       ~~~~~~~~~~~~~~~~~~~


Found 2 errors.

So, looking at the first error, it's clear that, in order for the type property of IAction to be acceptable, it must also include reset, which can be done by changing the definition of IAction to:

interface IAction {
    type: keyof IState | "reset";
    value?: string;
}

However, this doesn't resolve the second error, which is related to the fact that the value property is typed as string | undefined (due to the use of ?), but the various properties on IState are all typed as string (not allowing the possibility of being assigned undefined).

There are, as I see it, three options:

  • make IAction.value a non-optional string
  • make all the properties of IState optional strings
  • change IAction to a discriminated union (as per this answer)

The first two are both pretty straight-forward, but have far-reaching implications as to what uses of the are allowed or required to do, so let's look at the third. Changing the definition of IAction to:

type IAction = {
    type: keyof IState;
    value: string;
} | {
    type: "reset";
};

we have defined a type that is the union of two types, and instances of this type can be discriminated through use of narrowing. Thus, in the reducer:

const reducer = (state: IState, action: IAction) => {

    // `action` can be either:
    //   { type: keyof IState; value: string; }
    // or:
    //   { type: "reset"; }

    if (action.type === "reset") {

        // `action` can only be:
        //   { type: "reset" }
        // as the `type` property equals "reset"

        return initialState;
    }

    // `action` can only be:
    //   { type: keyof IState; value: string; }
    // as the `type` property _doesn't_ equal "reset"

    const result: IState = { ...state };
    result[action.type] = action.value;
    return result;
};

In this way, you've preserved the type safety of the type, without having to reduce the type safety of other types (eg making IState properties optional) or having needless values for some fields (eg making IAction.value non-optional and forcing the "reset" case to also have a value).

Handling Arbitrary Values

From your code, there is one place where an IAction is created with arbitrary values:

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    dispatch({ type: name, value });
};

which, as you've noted in your comment, will produce an error similar to:

main.ts:44:15 - error TS2322: Type 'string' is not assignable to type '"reset" | keyof IState'.

44     dispatch({type: name, value });
                 ~~~~

  main.ts:11:5
    11     type: keyof IState;
           ~~~~
    The expected type comes from property 'type' which is declared here on type 'IAction'


Found 1 error.

This is due to HTMLInputElement.name being typed string, which can be assigned values beyond the restricted set allowed (keyof IState). One option, as you've discovered, is to change the invocation of dispatch to:

dispatch({type: name as keyof IState, value });

using a type assertion to inform the type checker that name is of a compatible type. You can be a bit more rigorous and make use of a type predicate, which is a way of defining some runtime logic that, if met, would indicate that the type is compatible:

const isAction = (action: { type: string, value?: string }): action is IAction => {
    return action.type in ["email", "password", "passwordConfirmation", "username"];
}

used in onChange with something like:

const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    const action = { type: name, value };

    if (isAction(action)) {
        dispatch(action);
    }
};

One downside of this is the need to manually list all the keys of IState; if there is a change to the interface in future (addition or removal of a property) then the runtime type predicate would also need to be updated, otherwise it would no longer be valid.

  • Related