0
votes

I have a react form component that I'm trying to rewrite to typescript. I am trying to retrieve an object within another object in a for loop but I keep getting the following error

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 

I've attempted the solution presented in the following questions but I was still unable to have a working solution and I do not wish to set noImplicitAny to false in tsconfig.json:

  1. TypeScript: Object.keys return string[]

  2. TypeScript TS7015: Element implicitly has an 'any' type because index expression is not of type 'number'

  3. element implicitly has an 'any' type because type '{0}' has no index signature.

The code in question is - The problematic area can be found near the bottom:

import React from 'react';

import Button from '../../UI/Button/Button';
import Input from '../../UI/Input/Input';


type Config = {
    elementType: string;
    elementConfig: {
        type: string;
        placeholder: string;
        options: {
            value: string;
            displayValue: string;
        };
    };
    value: string;
    validation: {
        required: boolean;
        isEmail?: boolean;
        minLength?: number;
    };
    valid: boolean;
    touched: boolean;
    fieldActive: boolean;
    // [key: string]: string | Object;
}

interface SignInFormProps {
    isSignIn: boolean;
}

interface SignInFormState {
    controls: {
        email: Config;
        password: Config;
    };
    isSignUp: boolean;
    [key: string]: boolean | Object | Config;
}


class SignInForm extends React.Component<SignInFormProps, SignInFormState> {
    state = {
        controls: {
            email: {
                elementType: 'input',
                elementConfig: {
                    type: 'email',
                    placeholder: 'Your Email',
                    options: {
                        value: '',
                        displayValue: ''
                    },
                },
                value: '',
                validation: {
                    required: true,
                    isEmail: true
                },
                valid: false,
                touched: false,
                fieldActive: false
            },
            password: {
                elementType: 'input',
                elementConfig: {
                    type: 'password',
                    placeholder: 'Password',
                    options: {
                        value: '',
                        displayValue: ''
                    },
                },
                value: '',
                validation: {
                    required: true,
                    minLength: 6
                },
                valid: false,
                touched: false,
                fieldActive: false
            }
        },
        isSignUp: true
    }

    private activateField = ( controlName: keyof SignInFormState['controls'] ) => {
        do stuff...
    }

    ...

    render() {
        const formElementsArray: {id: string, config: Config}[] = [];

        // ################ The config value is causing the error ################
        for ( let key in this.state.controls ) {
            formElementsArray.push({
                id: key,
                config: this.state.controls[key] as Config
            });
        }

        let form = formElementsArray.map( formElement => (
            <Input
                blur={ ( event ) => this.disableFocus(event, formElement.id) }
                changed={ ( event ) => this.inputChangedHandler(event, formElement.id) }
                elementConfig={formElement.config.elementConfig}
                elementType={formElement.config.elementType}
                fieldActive={formElement.config.fieldActive}
                focus={ () => this.activateField(formElement.id) }
                invalid={!formElement.config.valid}
                key={formElement.id}
                shouldValidate={formElement.config.validation.required}
                touched={formElement.config.touched}
                value={formElement.config.value} />
        ));

If anybody has any ideas on how to resolve this while having clearly defined types and without using any then that would be helpful.

1

1 Answers

1
votes

First of all, there isn't a nice way to do this and it's still in discussion: https://github.com/Microsoft/TypeScript/issues/3500

Below are 2 potential ways to solve your issue.

  1. Declare the key outside of the loop:
const formElementsArray: { id: string; config: Config }[] = [];
let key: keyof typeof this.state.controls;
for (key in this.state.controls) {
  formElementsArray.push({
    id: key,
    config: this.state.controls[key],
  });
}
  1. Use Object.keys & cast the key to the desired type:
const formElementsArray: { id: string; config: Config }[] = (Object.keys(
  this.state.controls
) as (keyof typeof this.state.controls)[]).map((key) => ({
  id: key,
  config: this.state.controls[key],
}));

You could also make it clearer by using an enum for the control key

enum ControlKey {
  email = "email",
  password = "password",
}

interface SignInFormState {
  controls: {
    [key in ControlKey]: Config;
  };
  isSignUp: boolean;
  [key: string]: boolean | Object | Config;
}

then

1b.

const formElementsArray: { id: string; config: Config }[] = [];
let key: ControlKey;
for (key in this.state.controls) {
  formElementsArray.push({
    id: key,
    config: this.state.controls[key],
  });
}

or

2b.

const formElementsArray: { id: string; config: Config }[] = (Object.keys(
  this.state.controls
) as ControlKey[]).map((key) => ({
  id: key,
  config: this.state.controls[key],
}));

Hope this helps