Home > front end >  Narrowing object value type based on function parameter
Narrowing object value type based on function parameter

Time:11-15

So, I have enums:

export enum FilterName {
  Date = 'date',
  State = 'state',
}

export enum FilterField {
  CreatedAtStartDate = 'createdAtStartDate',
  CreatedAtEndDate = 'createdAtEndDate'
  State = 'state',
}

export type TDateFields = {
  min: FilterField.CreatedAtStartDate;
  max: FilterField.CreatedAtEndDate;
};

I have function:

export const getFilterField = (filterName: FilterName) =>
  ({
    [FilterName.Date]: {
      min: FilterField.CreatedAtStartDate,
      max: FilterField.CreatedAtEndDate,
    } as TDateFields,
    [FilterName.State]: FilterField.State,
  }[filterName]);

Now, I call the function like this:

const filterFields = getFilterField(FilterName.Date);

Doing filterFields.min will throw an error:

Property 'min' does not exist on type 'FilterField | TDateFields'

I can solve this by doing const filterFields = getFilterField(filterName) as TDateFields, but I would like to have type narrowing. Is that possible in a case like this and if yes, how?

CodePudding user response:

You can restrict your JSON key/value object to a type, and for a bonus, use generics to change the return type depending on the enum type.

export enum FilterName {
  Date = 'date',
  State = 'state',
}

export enum FilterField {
  CreatedAtStartDate = 'createdAtStartDate',
  CreatedAtEndDate = 'createdAtEndDate',
  State = 'state',
}

export type TDateFields = {
  min: FilterField.CreatedAtStartDate;
  max: FilterField.CreatedAtEndDate;
};

interface FieldData {
    [FilterName.Date]: TDateFields,
    [FilterName.State]: FilterField.State
}

export function getFilterField<T extends FilterName>(filterName: T): FieldData[T] {
  const data: FieldData = {
    [FilterName.Date]: {
      min: FilterField.CreatedAtStartDate,
      max: FilterField.CreatedAtEndDate,
    } as TDateFields,
    [FilterName.State]: FilterField.State,
  }

  return data[filterName]
}

const filterFields = getFilterField(FilterName.Date);

let min = filterFields.min
// ^? let min: FilterFIelds.CreatedAtStartData

TS Playground

CodePudding user response:

You can have no need for casting by using an overload:

export enum FilterName {
  Date = 'date',
  State = 'state',
}

export enum FilterField {
  CreatedAtStartDate = 'createdAtStartDate',
  CreatedAtEndDate = 'createdAtEndDate',
  State = 'state',
}

export type TDateFields = {
  min: FilterField.CreatedAtStartDate;
  max: FilterField.CreatedAtEndDate;
};

export function getFilterField (filterName: FilterName.Date): TDateFields
export function getFilterField (filterName: FilterName.State): { [FilterName.State]: FilterField.State }
export function getFilterField (filterName: FilterName) {
    switch (filterName) {
        case FilterName.Date:
          return {
            min: FilterField.CreatedAtStartDate,
            max: FilterField.CreatedAtEndDate,
          };
        
        case FilterName.State:
          return { [FilterName.State]: FilterField.State }

        // no need for a default case, the compiler understands
        // this is exhaustive
    }
};

// Has type TDateFields
const filterFields = getFilterField(FilterName.Date);

// No type error, compiler already narrowed during
// overload resolution.
filterFields.min;

Playground

I took the liberty of adding the missing member of the one enum (see my comment on the question). Note also that there's no cast on the TDateFields return case anymore.

  • Related