Home > OS >  Typescript - exclude reserved word from string
Typescript - exclude reserved word from string

Time:01-30

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 strings 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.

Playground link to code

  • Related