azaviruha
azaviruha

Reputation: 897

How to write type for accumulator object in Array.prototype.reduce when the accumulator is an object?

I have these types in my code:

    export interface KeyValue {
      [key: string]: any
    }

    export type FormErrors<V extends KeyValue> = {
      [K in keyof V]: string[]
    }

    export function getInitialErrors<V extends KeyValue> (data: V): FormErrors<V> {
      return Object.keys(data).reduce((acc, key) => (
        { ...acc, [key]: [] }
      ), {} as FormErrors<V>) // eslint-disable-line @typescript-eslint/consistent-type-assertions
    }

My question is: how can I remove direct type casting for accumulator in such situation?

I think that for achieving this I should already have default object with type FormErrors<V>. But the problem is that function getInitialErrors should create such default object.

Upvotes: 1

Views: 667

Answers (1)

Tim Perry
Tim Perry

Reputation: 13256

The main problem you have currently is that the type of FormErrors says that it has a key matching every key of V, and until the reduce finishes, that's not true.

You have a couple of options:

  • Pass generics to reduce to make the return type Partial<FormErrors<V>>, and cast to FormErrors at the end:

    export function getInitialErrors<V extends KeyValue> (data: V): FormErrors<V> {
        return Object.keys(data).reduce<Partial<FormErrors<V>>>((acc, key) => (
            { ...acc, [key]: [] }
        ), {}) as FormErrors<V>
    }
    

    You could simply a little by now dropping the function return type, since it's clearly defined by the cast anyway. This is arguably the most correct option (it really is partial during the reduce, and then you're clearly stating that you think it's now complete only at the end), but it's a bit messy.

  • Set the type of acc to Partial<FormErrors<V>> (i.e. tell the reduce the real type of acc). It surprises me a little, but this seem to work:

    export function getInitialErrors<V extends KeyValue> (data: V): FormErrors<V> {
        return Object.keys(data).reduce((acc: Partial<FormErrors<V>>, key) => (
            { ...acc, [key]: [] }
        ), {})
    }
    

    That's a pretty good balance between correctness & readability imo.

  • Make the keys in FormErrors optional (i.e. make it [K in keyof V]?: string[]). This works & makes your code totally correct, but might not work for the rest of your application.

  • Cast the {}, as you're currently doing. This works, but you're effectively lying to the compiler up front (at the beginning, it's really not a complete FormErrors) and then fixing it later before anything actually breaks.

Which one you want to pick depends on your use case, but I think those are the main options.

Upvotes: 3

Related Questions