1

Is it possible to make type-annotation like this?

name: OnlyAlfabetChars

where

name = "someAlfabetChars"

is allowed but

name = "some alpfabet"

or

name = "123"

is not allowed.

3
  • 2
    There is no specific type like this in TypeScript. You can use a generic type as a constraint and use a helper function, like this. Does that fully address your question? If so I can write up an answer; if not, what am I missing? Also, does "alphabet" mean just upper and lower A-Z, or does it include other character sets? Please mention @jcalz to notify me if you reply. Commented Oct 26, 2022 at 1:54
  • I'm still wondering: does "alphabet" mean just upper and lower A-Z, or does it include other character sets? Should "κάποιοαλφάβητο" be accepted? Should "какойтоалфавит" be accepted? Should "いくつかのアルファベット" be accepted? The first two are easy because those character sets make a distinction between uppercase and lowercase; the last one is hard. I can write up my answer but I hope you'll answer my question first Commented Oct 26, 2022 at 2:56
  • Looks a lot like TypeScript type string of specific characters or Typescript - type to check if the string contains only specific characters Commented Oct 26, 2022 at 17:17

1 Answer 1

7

There is no specific type in TypeScript that acts this way. There is an open feature request at microsoft/TypeScript#41160 for regular-expression validated string types that would probably give you this, but for now this is not implemented.

The closest you can get to this is to write a generic type that acts as a constraint on a string literal type, so that T extends OnlyAlphabet<T> if and only if T is a string literal type composed only of alphabetic characters. And that means you would need a generic helper function to infer the generic type argument, so instead of const x: OnlyAphabet = "abcdef"; you'd have to write const x = onlyAlphabet("abcdef").

So, how can we implement OnlyAlphabet<T>? We need to use template literal types to inspect the characters that make up a string, and in order to iterate over these characters we have to write it as a recursive conditional type. Here's one approach:

type OnlyAlphabet<T extends string, A extends string = ""> =
  T extends `${infer F}${infer R}` ?
  OnlyAlphabet<R, `${A}${Uppercase<F> extends Lowercase<F> ? "A" : F}`> :
  A;

const onlyAlphabet = <T extends string>(
  x: T extends OnlyAlphabet<T> ? T : OnlyAlphabet<T>
) => x;

So OnlyAphabet is a tail-recursive conditional type which checks each character in the input string. If it's an alphabetical character it leaves it alone; otherwise it replaces it with a capital A. So OnlyAlphabet<"abcdef"> is just "abcdef", but OnlyAlphabet<"abcd3f"> is "abcdAf".

Note that I take the shortcut that a character is alphabetical if and only if Uppercase<F> extends Lowercase<F> is false, using the intrinsic Uppercase<T> utility type and the Lowercase<T> utility type. This isn't perfect, but it works for a lot of character sets for which every alphabetical character has a distinct upper and lower case version, while non-alphabetical characters do not. If you have some other idea of what makes a character "alphabetical" and it can be implemented in TypeScript's type system, feel free to change it.

The onlyAlphabet helper function is generic in T that's constrained to string. I would love to write <T extends OnlyAlphabet<T>>(x: T)=>x, but that's an illegally circular constraint. Instead I use a quirk of inference and write <T extends string>(x: T extends OnlyAlphabet<T> ? T : OnlyAlphabet<T>)=>x. When you call it, the compiler will infer T to be the type of the x input, and then it will check T extends OnlyAlphabet<T>. If it succeeds, then the call succeeds because it amounts to <T extends string>(x: T)=>x which will work. If it fails, then the call fails, because it amounts to <T extends string>(x: OnlyAlphabet<T>) => x which will not match.


Let's test it out:

let x = onlyAlphabet("someAlphabetChars"); // okay
let y = onlyAlphabet("some alphabet"); // error!
// Argument of type '"some alphabet"' is not assignable to parameter of type '"someAalphabet"'
let z = onlyAlphabet("123"); // error!
// Argument of type '"123"' is not assignable to parameter of type '"AAA"'

Looks good. The compiler accepts a purely alphabetic string, but rejects ones with non-alphabetic characters and complains that the non-alpha character is not the letter A (so "some alphabet" is not "someAalphabet" and thus rejected).

In case it matters, this also works for other character sets that have distinct upper/lower case:

let w = onlyAlphabet("κάποιοαλφάβητο"); // okay
let v = onlyAlphabet("какойтоалфавит"); // okay

But fails for character sets with no such distinction:

let u = onlyAlphabet("いくつかのアルファベット"); // error!
// Argument of type '"いくつかのアルファベット"' is not assignable to parameter of type '"AAAAAAAAAAAA"'

So be careful.

Playground link to code

Sign up to request clarification or add additional context in comments.

2 Comments

> "you would need a generic helper function to infer the generic type argument" So currently it isn't possible to create a variable/constant that would enforce only letters, for example like this: TypeScript const valid: OnlyLetters = "someAlphabetChars"; const invalid: OnlyLetters = "some AlphabetChars"; ?
Right, that's what I was trying to convey with the opening sentence: "There is no specific type in TypeScript that acts this way."

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.