Home > front end >  Type narrowing in function parameters and infer them in function
Type narrowing in function parameters and infer them in function

Time:01-18

I am trying to make a date formatting function. There are two parameters I want to receive in function, which are type and format. Each type has its available formats.

For example,

type FruitType = 'apple' | 'banana'

const FORMATS: Record<FruitType, string[]> = {
  apple: ['HH:mm:ss', 'YYYY-MM-DD'],
  banana: ['YYYY-MM-DD HH:mm:ss', 'N,nnn,nnn seconds'],
};

And the function

const format = <T extends FruitType>(
  type: T,
  format: typeof FORMATS[T][number],
) => {
  if (type === 'apple') {
    if (format === '') { // I  want the format to be narrowed to 'HH:mm:ss' | 'YYYY-MM-DD', but the type is only typeof FORMATS[T][number]
    }
  }
};

How can I do following three things at once?

  1. make object that has keys which are FruitType and values of formats
  2. make formatting function which receive type(FruitType), then format(which is available under the first parameter fruit type)
  3. type is narrowed step by step (if I narrow fruit into apple, I want the format to be narrowed into 'HH:mm:ss' | 'YYYY-MM-DD')

I met this problem about three time at different challenges, but I still haven't get the answer... any good solution for this?

I tried to export function from object like

const formatters: {
  [key in FruitType]: (format: typeof FORMATS[key][number]) => string;
} = {
  apple: (format: typeof FORMATS["apple"]) => {
    if( format === "")
  },
  ...
};

but it did not work as well..

CodePudding user response:

The problem with your original approach is that the type of FORMATS is Record<FruitType, string[]>. If we ignore what is assigned to it for a moment, we can consider other alternatives that meet that description:

const validFormats1: Record<FruitType, string[]> = {
  apple: [],
  banana: [],
}

const validFormats2: Record<FruitType, string[]> = {
  apple: ['I am a string'],
  banana: ['Me too']
}

Now consider how TypeScript is supposed to determine the value of FORMATS['apple']. All it knows it that it can be any string[].

const FORMATS: Record<FruitType, string[]> = {
  apple: [ ... ],
  banana: [ ... ],
};

apple1 = FORMATS['apple'] // string[]

There are two solutions I am aware of for this type of problem:

  • Leverage as const, as long as you can deal with your types being readonly
  • Leverage discriminated union types, baking your definitions into the types themselves and avoiding the lookup

as const

type FruitType = 'apple' | 'banana'

const FORMATS_AS_CONST = {
  apple: ['HH:mm:ss', 'YYYY-MM-DD'],
  banana: ['YYYY-MM-DD HH:mm:ss', 'N,nnn,nnn seconds'],
} as const;

const format = (type: FruitType) => {
  if (type === 'apple') {
    FORMATS_AS_CONST[type] // readonly ["HH:mm:ss", "YYYY-MM-DD"]
  }
};

discriminated union types

type Apple = {
  type: 'apple';
  format: ['HH:mm:ss', 'YYYY-MM-DD'];
}
type Banana = {
  type: 'banana'
  format: ['YYYY-MM-DD HH:mm:ss', 'N,nnn,nnn seconds']
}
type Fruit = Apple | Banana;

const format = (fruit: Fruit) => {
  if (fruit.type === 'apple') {
    fruit.format // ["HH:mm:ss", "YYYY-MM-DD"]
  }
};

TypeScript Playground

  • Related