I wish to define an interface that does not allow to assign 'options' string on configNames field, but couldn't figure out how to make it work:
// First try
interface Options<T = string> {
configNames: T extends 'options' ? never: string;
}
// No validation because 'options' is subset of string, thus the type is always false, thus configName type is string
const options: Options = {
configNames: 'options'
}
// Second try
interface Options<T> {
configNames: T extends 'options' ? never: string;
}
// typescript error: Generic type 'Options<T>' requires 1 type argument(s).
const options: Options = {
configNames: 'options'
}
Would there be any workaround to achieve this?
CodePudding user response:
There is no specific type in TypeScript corresponding to "all string
s except for "options"
". That would require something like negated types as suggested in microsoft/TypeScript#4196 (and even as implemented in microsoft/TypeScript#29317 which was never merged). If TypeScript had negated types you could presumably write something like string & not "options"
. But it doesn't, so you can't. If using a specific Options
type is a requirement, then what you want is currently impossible.
So you're left with workarounds. There are no specific types, but you could make Options<T>
a generic type (as shown in your first try) and then use it as a constraint on a candidate type. And you could write a helper function to infer the generic type argument T
for you. So while there's no way to write const o: Options = {...}
and have it behave as desired, you could write const o = options({...})
and get a similar effect (and those aren't really very different if you squint at them).
The idea is to use conditional types to filter out "options"
from any string literal type in T
. You could use the Exclude<T, U>
utility type to do this:
interface Options<T extends string> {
configNames: Exclude<T, "options">
}
That's similar to your version. Also, if you want to prohibit string
itself (so that {configNames: someRandomString}
is rejected, since it could be "options"
), you could augment the definition to do that also:
interface Options<T extends string> {
configNames: string extends T ? never : Exclude<T, "options">
}
It's up to you whether you care about false positives or false negatives more. Anyway, the helper function would look like
const options = <T extends string>(o: Options<T>) => o;
And now you can test it out:
const bad = options({
configNames: 'options' // error!
});
const good = options({
configNames: 'somethingElse' // okay
});
// const good: Options<"somethingElse">
function foo(someRandomString: string) {
const maybe = options({
configNames: someRandomString // maybe error depending on needs
})
}
Looks good. You get the validation you care about. It's somewhat different from your desired approach, but it has the benefit of being possible to implement.