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'>