Cómo escribir un tipo de cadena que no contenga una cadena vacía en TypeScript
Una función de TypeScript escrita como sigue:
function propKeyMap(propKey:string):string {
//TODO
}
El propKey no puede ser un "" (cadena vacía). ¿Podemos escribir un tipo que no contenga una cadena vacía?
Respuesta #1
Solución usando tipos de plantillas literales
Desde TypeScript 4.1 puedes lograr esto usando tipos literales de plantilla. Sin embargo, es un poco verboso, porque tienes que definir un tipo unión de todas las cadenas posibles de longitud 1:
type Character = 'a' | 'b' | 'c' | ...;
type NonEmptyString = `${Character}${string}`;
Solución usando tipos condicionales
También hay otra solución (anterior a 4.1) usando tipos condicionales y un genérico:
type NonEmptyString<T extends string> = '' extends T ? never : T;
function propKeyMap<T extends string>(propKey: NonEmptyString<T>): string {
//TODO
}
Esta solución es útil para funciones ya que no tienes que especificar explícitamente el genérico al llamarla. Sin embargo, al usarla en cualquier otro lugar tendrías que definir el genérico, lo cual simplemente hará que sea el mismo tipo que usas para el genérico:
let x: NonEmptyString<'foo'>; // This is of type 'foo'
let y: NonEmptyString<string>; //This is of type never
Del mismo modo, no es posible pasar una cadena a la función (lo cual es correcto, ya que la cadena incluye una cadena vacía). Pero este hecho, combinado con la falta de posibilidad de tipar variables como NonEmptyString, significa que esto solo es útil para funciones que serán llamadas directamente con literales de cadena en lugar de 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)
Para los casos en los que esta limitación sea aceptable, recomendaría esta solución, ya que no implica definir el tedioso tipo de unión requerido para la primera solución.
Respuesta #2
Nota: esta respuesta fue escrita antes de que existieran los tipos condicionales y los tipos de literales de plantilla y, por lo tanto, está obsoleta (aunque todavía no existen tipos de resta y, por lo tanto, ningún tipo específico corresponde a string & not ""). La otra respuesta aquí habla sobre soluciones alternativas más recientes, así que no me detendré en ellas en esta respuesta.
La respuesta antigua es la siguiente:
No, no puedes hacer esto. Simplemente verifica el valor de propKey en tiempo de ejecución y lanza un error si está vacío.
Actualmente, TypeScript (a partir de la versión 2.5) carece de tipos de sustracción, por lo que no hay forma de decirle al compilador que un tipo es un string pero no "". Sin embargo, existen soluciones alternativas.
Puedes usar branding (ver la discusión en Microsoft/TypeScript #4895) para crear un subtipo de string, y luego intentar hacer cumplir la restricción de no vacío tú mismo, porque TypeScript no puede. Por ejemplo:
type NonEmptyString = string & { __brand: 'NonEmptyString' };
Ahora, no puedes simplemente asignar un valor string a una variable NonEmptyString:
const nope: NonEmptyString = 'hey'; // can't assign directly
Pero puedes crear una función que tome un string y devuelva un 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;
}
La función nonEmptyString() fallará en tiempo de ejecución si pasas un string vacío, así que mientras solo construyas objetos NonEmptyString con esta función, estás seguro. Además, si TypeScript sabe con certeza que has pasado un string vacío, el objeto devuelto será de tipo never (lo que esencialmente significa que no debería ocurrir). Por lo tanto, puede realizar una pequeña protección en tiempo de compilación contra strings vacíos:
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
Pero en realidad es solo un pequeño poco de protección en tiempo de compilación, ya que hay muchas ocasiones en las que TypeScript no se da cuenta de que un string está vacío:
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
Aun así, es mejor que nada... o podría serlo. La conclusión es que probablemente necesitas hacer afirmaciones en tiempo de compilación con verificaciones en tiempo de ejecución para la cadena vacía, sin importar qué.
Incluso si TypeScript pudiera expresar NonEmptyString de manera nativa como algo así como string - "", probablemente el compilador no sería lo suficientemente inteligente en la mayoría de los casos para deducir si el resultado de una manipulación de cadenas era o no un NonEmptyString. Es decir, nosotros sabemos que la concatenación de dos NonEmptyString debería tener una longitud de al menos dos, pero dudo que TypeScript lo hiciera:
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
Eso es porque el tipo que estás pidiendo está cerca de la cima de la pendiente resbaladiza hacia los tipos dependientes, que son geniales para la expresividad del desarrollador, pero no son geniales para la decisibilidad del compilador. Probablemente se podría hacer algo razonable a nivel de tipo para cadenas no vacías, pero en general todavía te encontrarás necesitando ayudar al compilador a decidir cuándo las cadenas son realmente no vacías.
Espero que eso ayude. ¡Buena suerte!
