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?
- make object that has keys which are FruitType and values of formats
- make formatting function which receive type(FruitType), then format(which is available under the first parameter fruit type)
- 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 beingreadonly
- 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"]
}
};