Ben Carp
Ben Carp

Reputation: 26518

Can't use a type parameter inside a generic Functions in Typescript

I have the following function :

const getDefaultFilterObjFromKeys: 
   <Key extends keyof FilterFields>(keys: Key[]) => IFiltersState<Key> =
      (keys) => {
         if (keys.length === 0){
            throw Error("empty Arr")
         }
         const defaults = {}
         Object.entries(_.pick(filterFields, keys)).forEach(
            ([key, filterField]) => {
               defaults[key] = filterField.defaultVal
            },
         )
         return defaults as IFiltersState<Key>
      }  

I get an error that the return type doesn't match the defined return type. I believe it does, but Typescript has a difficult time recognizing the actual return type for various reasons (it can't recognize that the keys array could not be empty, and there are a few calculations and manipulations), so either ignoring the error or casting is required. I'd rather cast, but in order to cast I need to be able to use the Key type parameter within the function. When I try to do so I get an error Cannot find name Key. Is there any syntax in which I could use the type parameter within the function to cast?

Upvotes: 1

Views: 875

Answers (3)

Nurbol Alpysbayev
Nurbol Alpysbayev

Reputation: 21851

This is how I'd write it if I were you. You didn't gave me an answer for my request (I've deleted that comment) so I had to improvise in some places. I removed lodash and replaced Object.entries with a more simple Object.keys.

Also, from my experience you should always separate types (interfaces) from implementations. That's what I did. This however doesn't mean that you won't need to duplicate some type variables (Key) when you need them in the implementation part as well.

The main idea you probably are missing is: You had your Key missing because you defined it in the constraining type, but not in the implementation function

const filterFields = {
    foo: {
        defaultVal: 123
    }
}

type FilterFields = typeof filterFields

interface IFiltersState<K> = {

}

interface GetDefaultFilterObjFromKeys {
    <Key extends keyof FilterFields>(keys: Key[]): IFiltersState<Key>
}

const getDefaultFilterObjFromKeys:
GetDefaultFilterObjFromKeys =
    <Key extends keyof FilterFields>(keys: Key[]) => {
        if (keys.length === 0) {
            throw Error("empty Arr")
        }
        const defaults: {
            [K in keyof FilterFields]: FilterFields[K]['defaultVal']
        } = {} as any;

        (Object.keys(filterFields) as Key[]).forEach(
            key => {
                if (!keys.includes(key)) {
                    return
                }
                defaults[key] = filterFields[key].defaultVal
            },
        )
        return defaults
    }  

Upvotes: 1

Ben Carp
Ben Carp

Reputation: 26518

Following @Robbie's answer and comments.

const function1: <I>(arg:I) => I = (arg) => {....}
const function2 = <I>(arg:I):I{...}

are not entirely equivalent. One of the distinctions is that in function 2 you could use the type parameter inside the body of the function. To be able to use the type parameter when writing the function in the first form, you have to define the type parameter as part of the function itself.

const function1: <I>(arg:I) => I = <I>(arg:I) => {....}

This makes our code even more verbose, so one might prefer the form used to write function2 in such cases.

Upvotes: 1

Robbie Milejczak
Robbie Milejczak

Reputation: 5770

It's because your signature is syntax is wrong, so you're ending up with a nested arrow function and Key is out of scope. There's really no need to define functions with const = () => unless you're worried about binding, so you can just make it more readable with the function keyword:

function getDefaultFilterObjFromKeys<Key extends keyof FilterFields>(keys: Key[]): IFiltersState<Key> {
  if (keys.length === 0){
    throw Error("empty Arr")
  }
  const defaults = {}
  Object.entries(_.pick(filterFields, keys)).forEach(
    ([k, filterField]) => {
      defaults[k] = filterField.defaultVal
    },
  )
  return defaults as IFiltersState<Key>
}

If you did want to use const the proper signature syntax would be:

const getDefaultFilterObjFromKeys2 = <Key extends keyof FilterFields>(keys: Key[]): IFiltersState<Key> => {
  if (keys.length === 0){
    throw Error("empty Arr")
  }
  const defaults = {}
  Object.entries(_.pick(filterFields, keys)).forEach(
    ([k, filterField]) => {
      defaults[k] = filterField.defaultVal
    },
  )
  return defaults as IFiltersState<K>
}

Upvotes: 2

Related Questions