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
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).
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
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
Playground
This isn't currently possibly 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
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With