I want to create a type that is an object, with properties values, and valuesMap. values should be an array of strings, and valuesMap should be an object whose keys are entries in values, and whose values are either a string or a boolean. I am struggling to define this type constraint in typescript. Here's what I've tried:
type FilterOptionBase<K extends string> = {
name: string;
values: K[];
multiselect: boolean;
isBoolean?: boolean;
valuesMap?: {
[key in K]: string | boolean;
};
};
type FilterOption = FilterOptionBase<string>
const myBadFilter: FilterOption = {
name: 'Status',
values: ["Online", "Offline"],
multiselect: false,
valuesMap: {
Online: true,
NotSure: false // <------ should error but doesnt
}
}
TypeScript playground
I feel like this should be relatively simple but the solution is escaping me. If this has been asked before, please direct me to the right place!
Also:
If isBoolean is defined as true, can we also contrain the values of valueMap to be a boolean, rather than a boolean | string?
CodePudding user response:
The reason your example isn't working is because this:
type FilterOption = FilterOptionBase<string>
...is passing string into FilterOptionBase, essentially doing this:
type FilterOptionBase = {
name: string;
values: string[];
multiselect: boolean;
isBoolean?: boolean;
valuesMap?: {
[key in string]: string | boolean;
};
};
So to type it correctly, you'd need to pass in the list of strings you'd expect when using the type, like this:
const myBadFilter: FilterOptionBase<"Online" | "Offline"> = {
name: 'Status',
values: ["Online", "Offline"],
multiselect: false,
valuesMap: {
Online: true,
NotSure: false // <------ errors now!
}
}
However, I think what you're actually trying to do is use the FilterOptionBase type as a template of sorts for all kinds of different objects, and the unfortunate part is that there's no way to do it that way. You can't have a type that is both reading from itself and constraining itself.
But you can access the values on an object that's passed into a function, so here's a workaround that I've used in the past to great effect:
const filterFactory = <K extends string>(v: FilterOptionBase<K>) => v
const myBadFilter = filterFactory({
name: 'Status',
values: ['Online', 'Offline'],
multiselect: false,
valuesMap: {
Online: true,
NotSure: false, // <------ it's an error!
}
})
"Also" Answer
Yes, it's possible to change it based on isBoolean being true. You'll just need to do a union:
type foo = {
isBoolean: true
value: boolean
} | {
isBoolean: false
value: string
}
Though it does get a little trickier because of the workaround you have to do with the factory function. Here it is:
type FilterOptionBase<
K extends string,
B extends boolean | undefined,
> = B extends true
? {
name: string
values: K[]
multiselect: boolean
isBoolean: B
valuesMap?: {
[key in K]: boolean
}
}
: {
name: string
values: K[]
multiselect: boolean
isBoolean?: B
valuesMap?: {
[key in K]: string
}
}
const filterFactory = <K extends string, B extends boolean | undefined>(
v: FilterOptionBase<K, B>,
) => v
const myBadFilter = filterFactory({
name: "Status",
values: ["Online", "Offline"],
multiselect: false,
isBoolean: true,
valuesMap: {
Online: true,
Offline: "foo", // <------ also an error!
},
})
const myOtherBadFilter = filterFactory({
name: "Status",
values: ["Online", "Offline"],
multiselect: false,
isBoolean: false,
valuesMap: {
Online: true, // <------ also an error!
Offline: "foo",
},
})
CodePudding user response:
How about explicitly passing the allowed values as type parameter when constructing FilterOption:
type FilterOption = FilterOptionBase<'Online' | 'Offline'>
