3

When I didn't add index signature in FormData:

interface FormData {
  applicationName: string;
  cluster: string;
  stackCode: number;
  GitHubToken: string;
}

enum FieldChangeType {
  TextInput,
  Toggle,
}

interface FieldAction {
  type: FieldChangeType;
  field: keyof FormData;
  payload?: FormData[keyof FormData];
}

function useFormRedux() {
  function reducer(preState: FormData, action: FieldAction) {
    const nextState: FormData = cloneDeep(preState);

    switch(action.type) {
      case FieldChangeType.TextInput:
        nextState[action.field] = action.payload!;
        // Error: Type 'string | number' is not assignable to type 'never'.
    }

    return nextState;
  }
}

When I add index signature in FormData, the error is gone:

interface FormData {
  [index: string]: boolean | number | string | string[]
  applicationName: string;
  cluster: string;
  stackCode: number;
  GitHubToken: string;
}

This is confuse me why ts will infer to never when I lack an index signature?

Pandy
  • 138
  • 1
  • 4

1 Answers1

0

First of all, FormData is a built in type which looks like this:

interface FormData {
    append(name: string, value: string | Blob, fileName?: string): void;
    delete(name: string): void;
    get(name: string): FormDataEntryValue | null;
    getAll(name: string): FormDataEntryValue[];
    has(name: string): boolean;
    set(name: string, value: string | Blob, fileName?: string): void;
    forEach(callbackfn: (value: FormDataEntryValue, key: string, parent: FormData) => void, thisArg?: any): void;
}

interface FormData {
    [Symbol.iterator](): IterableIterator<[string, FormDataEntryValue]>;
    /** Returns an array of key, value pairs for every entry in the list. */
    entries(): IterableIterator<[string, FormDataEntryValue]>;
    /** Returns a list of keys in the list. */
    keys(): IterableIterator<string>;
    /** Returns a list of values in the list. */
    values(): IterableIterator<FormDataEntryValue>;
}

Hence, I think it is a good idea to rename your custom interface.

Error:

Type 'string | number' is not assignable to type 'never'.
  Type 'string' is not assignable to type 'never'.(2322)

in

nextState[action.field] = action.payload!;

From type perspective action.payload might be string | number | undefined and action.field might be any key from CustomFormData. It means that there is no straight corelation between action.field and action.payload.

Further more,

interface FieldAction {
    type: FieldChangeType;
    field: keyof CustomFormData;
    payload?: CustomFormData[keyof CustomFormData];
}

is very unsafe. Consider this:

const unsafeAction: FieldAction = {
    type: FieldChangeType.TextInput,
    field: 'cluster',
    payload: 42
}

THis object has invalid representation, because payload should be string.

In order to fix it, you should make invalid state unrepresentable. Consider this example:


type FieldAction = {
    [Field in keyof CustomFormData]: {
        type: FieldChangeType,
        field: Field,
        payload?: CustomFormData[Field]
    }
}[keyof CustomFormData]

const ok: FieldAction = {
    type: FieldChangeType.TextInput,
    field: 'cluster',
    payload: 'str'
} // ok

const expected_error: FieldAction = {
    type: FieldChangeType.TextInput,
    field: 'cluster',
    payload: 42
} // error

We have created a union of all allowed states. Btw, TypeScript does not like mutations, you should always take it into account.

Here is my suggestion how to do it:

interface CustomFormData {
    applicationName: string;
    cluster: string;
    stackCode: number;
    GitHubToken: string;
}

enum FieldChangeType {
    TextInput,
    Toggle,
}

type FieldAction = {
    [Field in keyof CustomFormData]: {
        type: FieldChangeType,
        field: Field,
        payload?: CustomFormData[Field]
    }
}[keyof CustomFormData]


const makePayload = (state: CustomFormData, action: FieldAction)
    : CustomFormData => ({
        ...state,
        [action.field]: action.payload
    })

function useFormRedux() {
    function reducer(preState: CustomFormData, action: FieldAction) {
        const nextState: CustomFormData = null as any;

        switch (action.type) {
            case FieldChangeType.TextInput:
                return makePayload(preState, action)
        }

        return nextState;
    }
}

Playground

Here you can find offixial explanation

Here you can find more questions/answers about htis topic: first, second, almost a duplicate

Here you can find my article with some examples and links

Explanation of main utility

type FieldAction = {
    /**
     * This syntax mean iteration
     * See docs https://www.typescriptlang.org/docs/handbook/2/mapped-types.html
     */
    [Field in keyof CustomFormData]: {
        type: FieldChangeType,
        field: Field,
        payload?: CustomFormData[Field]
    }
}

/**
 * I have removed [keyof CustomFormData] from the end for the sake of readability
 * You can instead wrap FieldAction into Values utility type
 */

type Values<T> = T[keyof T]
{
    type Test1 = Values<{ a: 1, b: 2 }> // 1 | 2
}

type Result = Values<FieldAction>

Playground