Reputation: 7688
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
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
Upvotes: 42
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:
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
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
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
Reputation: 51
I solved this with the following, which:
Literals
, which is a union of literals.ExcludedLiterals
which is a subset of Literals
.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