Vincent
Vincent

Reputation: 5425

How to make TypeScript distinguish between a template literal string and any other string?

Playground link

I have a function that takes a second argument which is only required when the first argument is a string matching a template literal string. However, it appears that even when TypeScript knows that the first argument is such a string, it still matches the overload that takes a regular string:

type KnownString = `known${string}`;
type AnyString = string;

function doSomethingWithString(arg1: KnownString, onlyRequiredForKnownString: boolean): string;
function doSomethingWithString(arg1: AnyString, onlyRequiredForKnownString?: boolean): string;
function doSomethingWithString(arg1: KnownString | AnyString, onlyRequiredForKnownString?: boolean): string {
    return "The second argument is only required when `arg1` is a KnownString";
}

const thisIsAKnownString = "knownstring" as KnownString;

// @ts-expect-error Since TS knows that arg1 is a KnownString, it should require the second parameter:
doSomethingWithString(thisIsAKnownString);

This makes sense of course, since it is a regular string. Is there a way to tell that overload to match any regular string except KnownString?

(I did try with a conditional type like suggested in this similar question, but that does not appear to work with template literal strings.)

Upvotes: 1

Views: 295

Answers (2)

Vincent
Vincent

Reputation: 5425

I did find another solution with function overloads and somewhat more straightforward types:

type KnownString = `known${string}`;
type AnyString = string;
type NotKnown<T extends AnyString> = T extends KnownString ? never : T;

function doSomethingWithString(arg1: KnownString, onlyRequiredForKnownString: boolean): string;
function doSomethingWithString<T extends AnyString>(arg1: T & NotKnown<T>): string;
function doSomethingWithString(arg1: KnownString | AnyString, onlyRequiredForKnownString?: boolean): string {
    return "The second argument is only required when `arg1` is a KnownString";
}

const thisIsAKnownString = "knownstring" as KnownString;

// @ts-expect-error Since TS knows that arg1 is a KnownString, it should require the second parameter:
doSomethingWithString(thisIsAKnownString);

Playground link.

Upvotes: 0

I believe you dont need to overload your function.

Consider next example:

type IsKnown<T extends string> = T extends `known${string}` ? true:false

type Validator<T extends boolean> = 
  T extends true 
  ? [] 
  : [boolean];

function validation<Str extends string>(arg1: Str, ...validation: [...Validator<IsKnown<Str>>]): string {
    return "The second argument is only required when `arg1` is a KnownString";
}

const knownstring = "knownstring";

validation(knownstring); // does not require second argument
validation('unknown'); // requires second argument

Playground

Please keep in mind, that function foo(a:number,...args:[]){} evaluates to function foo(a:number){}

MOre information about this approach you can find in my blog

Take into account, that you can compose your validators:

type IsKnown<T> = T extends `known${string}` ? true : false

type StringNumber<T> = T extends `${number}` ? true : false


type Validator<T extends boolean> =
  T extends true
  ? []
  : [boolean];


function validation<Str extends string>(arg1: Str, ...validation: Validator<StringNumber<Str>> | Validator<IsKnown<Str>>): string {
  return "The second argument is only required when `arg1` is a KnownString";
}

const knownstring = "knownstring";

validation(knownstring); // does not require second argument
validation('2234'); // does not require second argument, because it is number
validation('wer'); //  requires second argument, because it is number

Upvotes: 2

Related Questions