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.
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:
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:
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:
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 quefirstName
se escribe comostring
, la devolución de llamada para el eventofirstNameChanged
espera que se le pase unstring
en el momento de la llamada. De manera similar, los eventos asociados conage
deberían ser llamados con un argumentonumber
. - 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.
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:
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:
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"'.
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:
- El literal usado en el primer argumento se captura como un tipo literal.
- Ese tipo literal se puede validar como si estuviera en la unión de atributos válidos en el genérico.
- El tipo de atributo validado se puede buscar en la estructura del genérico utilizando Acceso Indexado.
- 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
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
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
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
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
type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;
type UncomfortableGreeting = "hELLO WORLD"