Tipos literales de plantilla

Tipos literales de plantilla

Los tipos literales de plantilla se basan en tipos literales de cadena y tienen la capacidad de expandirse a muchas cadenas a través de uniones.

Tienen la misma sintaxis que cadenas literales de plantilla en JavaScript ↗, pero se utilizan en posiciones de tipo. Cuando se utiliza con tipos literales concretos, un literal de plantilla produce un nuevo tipo literal de cadena al concatenar el contenido.

Prueba este código ↗

type World = "world";
 
type Greeting = `hello ${World}`;
        
type Greeting = "hello world"

Cuando se usa una unión en la posición interpolada, el tipo es el conjunto de cada cadena literal posible que podría ser representada por cada miembro de la unión:

Prueba este código ↗

type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
 
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
          
type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"

Para cada posición interpolada en el literal de la plantilla, las uniones se multiplican de forma cruzada:

Prueba este código ↗

type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
type Lang = "en" | "ja" | "pt";
 
type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;
            
type LocaleMessageIDs = "en_welcome_email_id" | "en_email_heading_id" | "en_footer_title_id" | "en_footer_sendoff_id" | "ja_welcome_email_id" | "ja_email_heading_id" | "ja_footer_title_id" | "ja_footer_sendoff_id" | "pt_welcome_email_id" | "pt_email_heading_id" | "pt_footer_title_id" | "pt_footer_sendoff_id"

Generalmente recomendamos que las personas usen la generación anticipada (ahead-of-time) para uniones de cadenas grandes, pero esto es útil en casos más pequeños.

Uniones de cadenas en tipos

El poder de los literales de plantilla surge cuando se define una nueva cadena basada en información dentro de un tipo.

Considera el caso en el que una función (makeWatchedObject) agrega una nueva función llamada on() a un objeto pasado. En JavaScript, su llamada podría verse así: makeWatchedObject(baseObject). Podemos imaginar que el objeto base se parece a:

Prueba este código ↗

const passedObject = {
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26,
};

La función on que se agregará al objeto base espera dos argumentos, un eventName (un string) y un callback (una function).

El eventName debe tener la forma attributeInThePassedObject + "Changed"; por lo tanto, firstNameChanged se deriva del atributo firstName en el objeto base.

La función callback, cuando se llama:

  • Se debe pasar un valor del tipo asociado con el nombre attributeInThePassedObject; por lo tanto, dado que firstName se escribe como string, la devolución de llamada para el evento firstNameChanged espera que se le pase un string en el momento de la llamada. De manera similar, los eventos asociados con age deberían ser llamados con un argumento number.
  • Debe tener un tipo de devolución `voidv (para simplificar la demostración)

La firma de la función ingenua de on() podría ser: on(eventName: string, callback: (newValue: any) => void). Sin embargo, en la descripción anterior, identificamos restricciones de tipo importantes que nos gustaría documentar en nuestro código. Los tipos de literales de plantilla nos permiten incorporar estas restricciones a nuestro código.

Prueba este código ↗

const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26,
});
 
// makeWatchedObject ha agregado `on` al Object anónimo

person.on("firstNameChanged", (newValue) => {
  console.log(`firstName was changed to ${newValue}!`);
});

Observa que on escucha el evento "firstNameChanged", no solo "firstName". Nuestra ingenua especificación de on() podría hacerse más sólida si nos aseguráramos de que el conjunto de nombres de eventos elegibles estuviera restringido por la unión de nombres de atributos en el objeto observado con “Changed” agregado al final. Si bien nos sentimos cómodos haciendo este tipo de cálculo en JavaScript, es decir, Object.keys(passedObject).map(x => `${x}Changed`), los literales de plantilla dentro del sistema de tipos proporcionan un enfoque similar para la manipulación de cadenas:

Prueba este código ↗

type PropEventSource<Type> = {
    on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
};
 
/// Crea un "watched object" con un método `on` para que 
/// puedas observar los cambios en las propiedades.
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;

Con esto, podemos construir algo que genere errores cuando se le dé la propiedad incorrecta:

Prueba este código ↗

const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26
});
 
person.on("firstNameChanged", () => {});
 
// Evita errores humanos fáciles (usando la clave en lugar del nombre del evento)
person.on("firstName", () => {});
 
// Es resistente a errores tipográficos
person.on("frstNameChanged", () => {});
Argument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.2345Argument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.
Error generado
Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.

Inferencia con literales de plantilla

Observa que no nos beneficiamos de toda la información proporcionada en el objeto pasado original. Dado el cambio de un firstName (es decir, un evento firstNameChanged), deberíamos esperar que la devolución de llamada reciba un argumento de tipo string. De manera similar, la devolución de llamada para un cambio en age debe recibir un argumento number. Estamos usando ingenuamente any para tipar el argumento de la “devolución de llamada” (callback). Nuevamente, los tipos literales de plantilla permiten garantizar que el tipo de datos de un atributo sea el mismo tipo que el primer argumento de la devolución de llamada de ese atributo.

La idea clave que hace esto posible es la siguiente: podemos usar una función con un genérico tal que:

  1. El literal usado en el primer argumento se captura como un tipo literal.
  2. Ese tipo literal se puede validar como si estuviera en la unión de atributos válidos en el genérico.
  3. El tipo de atributo validado se puede buscar en la estructura del genérico utilizando Acceso Indexado.
  4. Esta información de escritura se puede luego aplicar para garantizar que el argumento de la función de devolución de llamada es del mismo tipo

Prueba este código ↗

type PropEventSource<Type> = {
    on<Key extends string & keyof Type>
        (eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void): void;
};
 
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
 
const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26
});
 
person.on("firstNameChanged", newName => {
                                
(parameter) newName: string
    console.log(`new name is ${newName.toUpperCase()}`);
});
 
person.on("ageChanged", newAge => {
                          
(parameter) newAge: number
    if (newAge < 0) {
        console.warn("warning! negative age");
    }
})

Aquí convertimos on en un método genérico.

Cuando un usuario llama con la cadena "firstNameChanged", TypeScript intentará inferir el tipo correcto para Key. Para hacer eso, comparará Key con el contenido antes de "Changed" e inferirá la cadena "firstName". Una vez que TypeScript se da cuenta de eso, el método on puede recuperar el tipo de firstName en el objeto original, que es string en este caso. De manera similar, cuando se llama con "ageChanged", TypeScript encuentra el tipo de la propiedad age que es number.

La inferencia se puede combinar de diferentes maneras, a menudo para deconstruir cadenas y reconstruirlas de diferentes maneras.

Tipos de manipulación de cadenas intrínsecas

Para ayudar con la manipulación de cadenas, TypeScript incluye un conjunto de tipos que se pueden usar en la manipulación de cadenas. Estos tipos vienen integrados en el compilador para mejorar el rendimiento y no se pueden encontrar en los archivos .d.ts incluidos con TypeScript.

Uppercase<StringType>

Convierte cada carácter de la cadena a la versión en mayúsculas.

Ejemplo

Prueba este código ↗

type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase<Greeting>
           
type ShoutyGreeting = "HELLO, WORLD"
 
type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`
type MainID = ASCIICacheKey<"my_app">
       
type MainID = "ID-MY_APP"

Lowercase<StringType>

Convierte cada carácter de la cadena al equivalente en minúsculas.

Ejemplo

Prueba este código ↗

type Greeting = "Hello, world"
type QuietGreeting = Lowercase<Greeting>
          
type QuietGreeting = "hello, world"
 
type ASCIICacheKey<Str extends string> = `id-${Lowercase<Str>}`
type MainID = ASCIICacheKey<"MY_APP">
       
type MainID = "id-my_app"

Capitalize<StringType>

Convierte el primer carácter de la cadena en un equivalente en mayúscula.

Ejemplo

Prueba este código ↗

type LowercaseGreeting = "hello, world";
type Greeting = Capitalize<LowercaseGreeting>;
        
type Greeting = "Hello, world"

Uncapitalize<StringType>

Convierte el primer carácter de la cadena a un equivalente en minúscula.

Ejemplo

Prueba este código ↗

type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;
              
type UncomfortableGreeting = "hELLO WORLD"
Última actualización