Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write a string type that does not contain an empty string in TypeScript

Tags:

typescript

A function of TypeScript wroten as below:

function propKeyMap(propKey:string):string {
  //TODO
}

The propKey can't be an ""(empty string). Can we write a type which does not contain an empty string?

like image 974
xiang Avatar asked Sep 16 '17 11:09

xiang


2 Answers

Solution using template literal types

Since TypeScript 4.1 you can achive this using template literal types. However, it's a bit verbose, because you have to define a union type of all possible strings of length 1:

type Character = 'a' | 'b' | 'c' | ...;
type NonEmptyString = `${Character}${string}`;

Solution using conditional types

There's another solution (prior to 4.1) too using conditional types and a generic:


type NonEmptyString<T extends string> = '' extends T ? never : T;
function propKeyMap<T extends string>(propKey: NonEmptyString<T>): string {
  //TODO
}

This solution is useful for functions as you don't have to explicitly specify the generic when calling it. However, when using it anywhere else you'd have to define the generic which then will just make it the same type you use for the generic:

let x: NonEmptyString<'foo'>; // This is of type 'foo'
let y: NonEmptyString<string>; //This is of type never

Likewise, it is not possible to pass a string to the function (which is correct, as string includes an empty string). But this fact, combined with the lack of possibility to type variables as NonEmptyString, means, that this is only useful for functions that will directly be called with string literals rather than variables.

propKeyMap('foo'); // Compiles (as expected)
propKeyMap('');    // Errors   (as expected)

const x: 'foo' = 'foo';
propKeyMap(x);     // Compiles (as expected)

const y: string = 'foo';
propKeyMap(y);     // Errors   (unexpected)

For cases where this limitation is acceptable, I'd recommend this solution though, as it doesn't include defining the cumbersome union type required fo the first solution.

like image 196
Grochni Avatar answered Oct 05 '22 13:10

Grochni


Note: this answer was written before conditional types and template literal types existed and is therefore obsolete (although there are still no subtraction types and so no specific type corresponds to string & not ""). The other answer here talks about newer workarounds, so I won't belabor them in this answer.

Old answer follows:


No, you can't do this. Just check the propKey value at runtime and throw an error if it is empty.

TypeScript currently (as of v2.5) lacks subtraction types, so there's no way to tell the compiler that a type is a string but not "". There are, however, workarounds.

You can use branding (see discussion in Microsoft/TypeScript #4895) to create a subtype of string, and then try to enforce the non-empty constraint yourself, because TypeScript can't. For example:

type NonEmptyString = string & { __brand: 'NonEmptyString' };

Now, you can't just assign a string value to a NonEmptyString variable:

const nope: NonEmptyString = 'hey'; // can't assign directly

But you can create a function which takes a string and returns a NonEmptyString:

function nonEmptyString(str: ""): never;
function nonEmptyString(str: string): NonEmptyString;
function nonEmptyString(str: string): NonEmptyString {
  if (str === '')
    throw new TypeError('empty string passed to nonEmptyString()');
  return str as NonEmptyString;
}

The function nonEmptyString() will blow up at runtime if you pass in an empty string, so as long as you only construct NonEmptyString objects with this function, you are safe. Additionally, if TypeScript knows for a fact that you've passed in an empty string, the returned object will be of type never (essentially meaning that it shouldn't happen). So it can do a little bit of compile-time guarding against empty strings:

const okay = nonEmptyString('hey');
okay.charAt(0); // still a string

const oops = nonEmptyString(''); // will blow up at runtime
oops.charAt(0); // TypeScript knows that this is an error 

But it really is just a little bit of compile-time guarding, since there are many times when TypeScript doesn't realize that a string is empty:

const empty: string = ""; // you've hidden from TS the fact that empty is ""
const bigOops = nonEmptyString(empty); // will blow up at runtime
bigOops.charAt(0); // TypeScript doesn't realize that it will blow up

Still, it's better than nothing... or it might be. The bottom line is that you probably need to do compile time assertions with runtime checks for the empty string, no matter what.

Even if TypeScript could express NonEmptyString natively as something like string - "", the compiler probably wouldn't be smart enough in most cases to deduce that the result of a string manipulation was or was not a NonEmptyString. I mean, we know that a concatenation of two NonEmptyStrings should have a length of at least two, but I doubt TypeScript would:

declare let x: NonEmptyString; 
declare let y: NonEmptyString; 
const secondCharBad: NonEmptyString = (x + y).charAt(1); // won't work
const secondCharGood = nonEmptyString((x + y).charAt(1)); // okay

That's because the type you're asking for is near the very top of the slippery slope down to dependent types, which are great for developer expressivity, but not great for compiler decidability. Probably something reasonable could be done at the type level for non-empty strings, but in general you'll still find yourself needing to help the compiler decide when strings are actually non-empty.


Hope that helps. Good luck!

like image 25
jcalz Avatar answered Oct 05 '22 13:10

jcalz