Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

typescript

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
like image 801
Marcus Riemer Avatar asked Jul 20 '18 12:07

Marcus Riemer


3 Answers

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

like image 147
Titian Cernicova-Dragomir Avatar answered Oct 23 '22 09:10

Titian Cernicova-Dragomir


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
like image 36
ccarton Avatar answered Oct 23 '22 11:10

ccarton


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!
like image 1
ecmanaut Avatar answered Oct 23 '22 09:10

ecmanaut