Marcus Riemer
Marcus Riemer

Reputation: 7688

Type for "every possible string value except ..."

Is it possible to define a type that may have every string-value assigned except a few specified ones? I would like to express something along the lines of this (non-compiling) example:

type ReservedNames = "this" | "that"
type FooName = string - ReservedNames;
const f1 : FooName = "This" // Works
const f2 : FooName = "this" // Should error

Upvotes: 82

Views: 32634

Answers (5)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249726

There isn't a general solution to this problem since there is no way to express in the typescript type system the fact that a string can be any value except a list. (One might think the conditional type Exclude<string, ReservedNames> would work but it does not, it just evaluates back to string).

As a work around, if we have a function and we specifically want to not allow certain constants to be passed in we can us a conditional type to check for ReservedNames and, if the passed in parameter is ReservedNames then type the input parameter in such a way it is effectively impossible to satisfy (using an intersection type).

// The following works
function withName<T extends string>(
  v: T & (T extends ReservedNames ? "Value is reserved!" : {}),
) {
  return v;
}

withName("this"); // Type '"this"' is not assignable to type '"Value is reserved!"'.
withName("This"); // ok
// The following will NOT work!
type ReservedNames = "this" | "that"
type FooName = Exclude<string, ReservedNames>;

const f1 : FooName = "This" // Works
const f2 : FooName = "this" // One might expect this to work but IT DOES NOT as FooName is just evaluates to string

Playground

Upvotes: 42

Cory Mawhorter
Cory Mawhorter

Reputation: 1821

Edit: Doh. I need to stop skimming text... my answer does not solve OP's problem. Trying it with type FooName = keyof RestrictedRecord does not work. I'll leave my answer instead of deleting FWIW.

Original post:

this seems to work for me (typescript v5.3.2), but only very slightly improves the vague errors. but it seems more readable/maintainable to me:

Playground

const restrictedKeys = ['this', 'that'] as const;
type RestrictedKey = typeof restrictedKeys[number];

// i'd wager order is important here with restricted coming second, but i didn't confirm
type RestrictedRecord = Record<string, string> & Partial<Record<RestrictedKey, never>>;

const good1: RestrictedRecord = {hello: 'world'}; // ok
const bad1: RestrictedRecord = {that: 'world'}; // error is "that" must be undefined
const bad2: RestrictedRecord = {that: undefined}; // error is "that" must be string, but at least mentions the RestrictedRecord type

Upvotes: 0

ccarton
ccarton

Reputation: 3666

This isn't currently possible in TypeScript, however you can create a generic type that can handle many of the practical use cases if you add the concrete string value as a parameter of FooName.

type ReservedNames = "this" | "that"
type NotA<T> = T extends ReservedNames ? never : T
type NotB<T> = ReservedNames extends T ? never : T
type FooName<T> = NotA<T> & NotB<T>

const f1: FooName<'This'> = 'This' // works
const f2: FooName<'this'> = 'this' // error

const f3: FooName<string> = 'this' //error
const f4: FooName<any> = 'this' // error
const f5: FooName<unknown> = 'this' // error

And in a function it works as expected if you make the function generic on the string value:

function foo<T extends string> (v: FooName<T>) {
  ...
}

foo('this') // error
foo('This') // works

Upvotes: 29

ecmanaut
ecmanaut

Reputation: 5150

A warning:

I wanted this for some react components that I wanted to take some string-valued prop keys that should be anything but a set of localization system lookup constants, and while the accepted answer from ccarton above does the trick (applied here, too: Playgrounds), it is also worth mentioning that typescript's error messages when your code fails to meet the type constraint, are completely rubbish, and actively misleading – example from the playground link / code below:

<DemandsNonLocKeys title={"illegal"} text={"!"}/>; // fails, as wanted 🤠

Hover the marked-as-illegal title attribute's squigglies (yes, not text), and you currently (* with typescript 4.7.2, in case this ever improves in some direction) see:

(property) title: "!"
Type '"illegal"' is not assignable to type '"!"'.(2322)

So, while this hack is excellent for things like preventing commit hooks from committing buggy code, it relies on developers to have tribal knowledge about what is wrong when these errors strike, as the error message is completely off the rails.

Complete example code, in case Playgrounds ever dies:

import * as React from "react";

const translations = {
    "illegal": "otillåten",
    "forbidden": "förbjuden"
} as const;

type LocDict = typeof translations;
type LocKey = keyof LocDict;
type LocString = LocDict[LocKey]; // stricter constraint than NotLocKey

type NotA<T> = T extends LocKey ? never : T;
type NotB<T> = LocKey extends T ? never : T;
export type NotLocKey<T> = NotA<T> & NotB<T>;

function DemandsNonLocKeys<T extends string>({ title, text }: {
    title: NotLocKey<T>,
    text?: NotLocKey<T>
}) {
    return <>{text}: {title}</>;
};

<DemandsNonLocKeys title={"illegal"} text={"!"}/>;     // fails, as wanted 🤠
<DemandsNonLocKeys title={"not"} text={"forbidden"}/>; // fails, as wanted 🤠
<DemandsNonLocKeys title={"anything"} text={"goes"}/>; // all non-LocKey: ok!

Upvotes: 1

ericrabil
ericrabil

Reputation: 51

I solved this with the following, which:

  1. Creates an object whose keys are Literals, which is a union of literals.
  2. Omits from the object any literals in type ExcludedLiterals which is a subset of Literals.
  3. Returns the keyof resolution of the object.
type OmitLiteral<Literals extends string | number, ExcludedLiterals extends Literals> = keyof Omit<{ [Key in Literals]: never }, ExcludedLiterals>;

It's a bit convoluted, but it works.

type BaseUnion = "abc" | "def" | "ghi"; // "abc" | "def" | "ghi"
type OmittedUnion = OmitLiteral<BaseUnion, "ghi">; // "abc" | "def"

Upvotes: -3

Related Questions