Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Descripción de TypeScript
De muchas maneras, resulta útil pensar en TypeScript por sus propios méritos. La especificación del lenguaje de TypeScript hace referencia a TypeScript como una “un azúcar sintética para JavaScript”. Eso es verdadero y probablemente un paso fundamental para llegar a la audiencia de destino del idioma, los programadores del lado del cliente que actualmente usa JavaScript.
Es necesario conocer JavaScript antes de poder comprender TypeScript. De hecho, la especificación de lenguaje (puede leerlo en bit.ly/1xH1m5B) a menudo describe las estructuras de TypeScript en términos del código JavaScript resultante. Sin embargo, es igualmente útil considerar a TypeScritp como lenguaje en si mismo que comparte características con JavaScript.
Por ejemplo, al igual que C#, TypeScript es un lenguaje de tipo de datos, que le ofrece compatibilidad con IntelliSense y comprobación de tiempo de compilación, entre otras características. Al igual que C#, TypeScript incluye expresiones genéricas y lambda (o su equivalente).
Pero TypeScript, por supuesto no es C#. Comprender qué es único acerca de TypeScript es tan importante como comprender qué TypeScript comparte con el idioma del servidor que está usando actualmente. El sistema de tipo TypeScript es diferente (y más simple) que C#. TypeScript aprovecha su conocimientos de otros modelos de objeto, de una manera única y ejecuta la herencia, de manera diferente que C#. Dado que TypeScript compila en JavaScript, TypeScript comparte muchos de sus aspectos fundamentales con JavaScript, a diferencia de C#.
La pregunta continúa siendo “¿Preferiría escribir el código de cliente en este lenguaje o en JavaScript?”
TypeScript tiene datos con tipos
TypeScript no cuenta con muchos tipos de datos incorporados que pueda usar para declarar variables, simplemente cadena, número y booleanos. Esos tres tipos son un subtipo de cualquier tipo (que también puede usarlo al declarar variables). También puede establecer o probar variables declaradas con esos cuatro tipos contra los tipos nulos o no definidos. También puede declarar métodos como vacíos, que indican que no devuelven un valor.
Este ejemplo declara una variable como cadena:
var name: string;
Puede ampliar este sistema de tipos simple con valores enumerados y cuatro clases de tipos de objeto: interfaces, clases, matrices y funciones. Por ejemplo, el siguiente código define una interfaz (una clase de tipo de objeto) con el nombre ICustomerShort. La interfaz incluye dos miembros: una propiedad llamada Id y un método llamado CalculateDiscount:
interface ICustomerShort
{
Id: number;
CalculateDiscount(): number;
}
Como en C#, puede usar interfaces al declarar variables y tipos de devolución. Este ejemplo declara la variable cs como tipo ICustomerShort:
var cs: ICustomerShort;
También puede definir tipos de objeto como clases, que a diferencia de las interfaces, pueden contener código ejecutable. Este ejemplo define una clase llamada CustomerShort con una propiedad y un método:
class CustomerShort
{
FullName: string;
UpdateStatus( status: string ): string
{
...manipulate status...
return status;
}
}
Al igual que las versiones recientes de C#, no es necesario proporcionar código de implementación al definir una propiedad. La declaración simple del nombre y el tipo es suficiente. Las clases pueden implementar una o más interfaces, como se muestra en la Figura 1, que agrega mi interfaz ICustomerShort, con su propiedad para mi clase CustomerShort.
Figura 1 Agregar una interfaz a una clase
class CustomerShort implements ICustomerShort
{
Id: number;
FullName: string;
UpdateStatus(status: string): string
{
...manipulate status...
return status;
}
CalculateDiscount(): number
{
var discAmount: number;
...calculate discAmount...
return discAmount;
}
}
Como lo muestra la Figura 1, la sintaxis de implementación de una interfaz es tan simple en TypeScript como en C#. Para implementar los miembros de la interfaz, simplemente agregue miembros con el mismo nombre en lugar de vincular el nombre de la interfaz con los miembros de la clase correspondiente. En este ejemplo, simplemente agregué Id y CalculateDiscount a la clase para implementar ICustomerShort. TypeScript también le permite usar literales de tipo de objeto. Este código establece la variable cst en un literal de objeto que contiene una propiedad y un método:
var csl = {
Age: 61,
HaveBirthday(): number
{
return this.Age++;
}
};
Este ejemplo usa un tipo de objeto para especificar el valor de devolución del método UpdateStatus:
UpdateStatus( status: string ): {
status: string; valid: boolean }
{
return {status: "New",
valid: true
};
}
Además de los tipos de objeto (clase, interfaz, literal y matriz), también puede definir los tipos de función que describen una firma de función. El siguiente código reescribe CalculateDiscount de mi clase CustomerShort para aceptar un solo parámetro llamado discountAmount:
interface ICustomerShort
{
Id: number;
CalculateDiscount( discountAmount:
( discountClass: string,
multipleDiscount: boolean ) => number): number
}
Ese parámetro se define con un tipo de función que acepte dos parámetros (uno de cadena, uno de booleano) y devuelve un número. Si es programador de C#, podría encontrar que la sintaxis se parece mucho más a una expresión lambda.
Una clase que implementa esta interfaz tendría la apariencia de la Figura 2.
Figura 2 Esta clase implementa la interfaz adecuada
class CustomerShort implements ICustomerShort
{
Id: number;
FullName: string;
CalculateDiscount( discountedAmount:
( discountClass: string,
multipleDiscounts: boolean ) => number ): number
{
var discAmount: number;
...calculate discAmount...
return discAmount;
}
}
Al igual que las versiones recientes de C#, TypeScript también infiere el tipo de datos de una variable a partir del valor en el cual se inicializa la variable. En este ejemplo, TypeScript que la variable myCust pertenece a CustomerShort:
var myCust= new CustomerShort();
myCust.FullName = "Peter Vogel";
Al igual que C#, puede declarar variables que usan una interfaz y luego, establecer la variable para un objeto que implemente esa interfaz:
var cs: ICustomerShort;
cs = new CustomerShort();
cs.Id = 11;
cs.FullName = "Peter Vogel";
Por último, también puede usar estos parámetros de tipo (que se parecen sospechosamente a los genéricos en C#) para permitir que el código de invocación especifique el tipo de dato que se utilizará. Este ejemplo, permite que el código que crea la clase establezca el tipo de dato de la propiedad de Id:
class CustomerTyped<T>
{
Id: T;
}
Este código establece el tipo de dato de la propiedad de Id en un cadena antes de usarlo:
var cst: CustomerTyped<string>;
cst = new CustomerTyped<string>();
cst.Id = "A123";
Para aislar las clases, las interfaces y otros miembros públicos y evitar las colisiones de nombre, puede declarar estas estructuras dentro de módulos similares a los espacios de nombres de C#. Deberá marcar esos elementos que desea que se encuentren disponibles en otros módulos con la palabra clave de exportación. El módulo en Figura 3 exporta dos interfaces y una clase.
Figura 3 Exportación de dos interfaces y una clase
module TypeScriptSample
{
export interface ICustomerDTO
{
Id: number;
}
export interface ICustomerShort extends ICustomerDTO
{
FullName: string;
}
export class CustomerShort implements ICustomerShort
{
Id: number;
FullName: string;
}
Para usar los componentes exportados, puede colocar un prefijo en el nombre de componente con el nombre de módulo como en este ejemplo:
var cs: TypeScriptSample.CustomerShort;
O bien, puede usar la palabra clave de importación de TypeScript para establecer un acceso directo al módulo:
import tss = TypeScriptSample;
...
var cs:tss.CustomerShort;
TypeScript es flexible en cuanto a la escritura de datos
Todo esto debería parecerle conocido si es programador de C#, a excepción quizás de la reversión de declaraciones de variable (nombre de variable primero, tipo de datos, segundo y literales de objeto. Sin embargo, casi toda la escritura de datos en TypeScript es opcional. La especificación describe los tipos de datos como “anotaciones”. Si omite tipos de datos (el TypeScript no infiere el tipo de datos), los tipos de datos son cualquier tipo de forma predeterminada.
TypeScript no requiere una correspondencia estricta con el tipo de datos, tampoco. Para determinar la compatibilidad, TypeScript usa lo que la especificación denomina “subtipo estructural”. Esto es similar a lo que normalmente se denomina “duck typing”. En TypeScript, dos clases se consideran idénticas si tienen miembros con los mismos tipos. Por ejemplo, esta es una clase Customer-Short que implementa una interfaz llamada ICustomerShort:
interface ICustomerShort
{
Id: number;
FullName: string;
}
class CustomerShort implements ICustomerShort
{
Id: number;
FullName: string;
}
Esta es una clase llamada CustomerDeviant que en apariencia es similar a la clase CustomerShort:
class CustomerDeviant
{
Id: number;
FullName: string;
}
Gracias al subtipo estructural, puedo usar CustomerDevient con variables definidas con mi clase CustomerShort o la interfaz ICustomerShort. Estos ejemplos usan CustomerDeviant de manera intercambiable con variables declaradas como CustomerShort o ICustomerShort:
var cs: CustomerShort;
cs = new CustomerDeviant
cs.Id = 11;
var csi: ICustomerShort;
csi = new CustomerDeviant
csi.FullName = "Peter Vogel";
Esta flexibilidad le permite asignar literales de objeto TypeScript a variables declaradas como clases o interfaces, siempre que sean estructuralmente compatibles, como las que están aquí:
var cs: CustomerShort;
cs = {Id: 2,
FullName: "Peter Vogel"
}
var csi: ICustomerShort;
csi = {Id: 2,
FullName: "Peter Vogel"
}
Esto conduce a las características específicas de TypeScript, los supertipos y los subtipos que conducen al problema general de capacidad de asignación que aquí pasaré por alto. Estas características le permitirían a CustomerDeviant, por ejemplo, contar con miembros que no están presentes en CustomerShort sin hacer que mi código de muestra falle.
TypeScript tiene clase
La especificación de TypeScript hace referencia al lenguaje como la implementación de “el patrón de clase [que usa] cadenas de prototipo para implementar muchas variaciones en los mecanismos de herencia orientados al objeto.” En la práctica, significa que TypeScript no solo está tipificado pro datos, sino eficazmente orientado al objeto.
De la misma manera que la interfaz de C# puede heredar una interfaz básica, una interfaz TypeScript puede ampliar otra interfaz, aún si esa otra interfaz se define en otro módulo. Este ejemplo amplía la interfaz ICustomerShort para crear una nueva interfaz llamada ICustomerLong:
interface ICustomerShort
{
Id: number;
}
interface ICustomerLong extends ICustomerShort
{
FullName: string;
}
La interfaz ICustomerLong tendrá dos miembros: FullName e Id. En la interfaz combinada, los miembros de la interfaz aparecen primero. Por lo tanto, mi interfaz ICustomerLong equivale a esta interfaz:
interface ICustomerLongPseudo
{
FullName: string;
Id: number;
}
Una clase que implementa ICustomerLong necesitaría ambas propiedades:
class CustomerLong implements ICustomerLong
{
Id: number;
FullName: string;
}
Las clases pueden ampliar otras clases de la misma manera que una interfaz puede ampliar otra. La clase de la Figura 4 amplía CustomerShort y agrega una nueva propiedad a la definición. Usa captadores y establecedores explícitos para definir propiedades (aunque no de una manera particularmente útil).
Figura 4 Propiedades definidas con captadores y establecedores
class CustomerShort
{
Id: number;
}
class CustomerLong extends CustomerLong
{
private id: number;
private fullName: string;
get Id(): number
{
return this.id
}
set Id( value: number )
{
this.id = value;
}
get FullName(): string
{
return this.fullName;
}
set FullName( value: string )
{
this.fullName = value;
}
}
TypeScript aplica la mejor práctica de acceso a campos internos (como id y fullName) a través de una referencia a la clase (esta). Las clases también pueden tener funciones constructoras que incluyen una característica que C# acaba de adoptar: definición automática de campos. La función constructora en una clase TypeScript debe designarse como constructora y sus parámetros públicos se definen automáticamente como propiedades y se inicializan a partir de los valores que les pasaron. En este ejemplo, el constructor acepta un solo parámetro llamado Company de cadena de tipo:
export class CustomerShort implements ICustomerShort
{
constructor(public Company: string)
{ }
Dado que el parámetro Company se define como público, la clase también obtiene una propiedad pública llamada Company inicializada a partir del valor que se le pasó al constructor. Gracias a esta característica, la variable comp se establecerá en “PH&VIS,” como en este ejemplo:
var css: CustomerShort;
css = new CustomerShort( "PH&VIS" );
var comp = css.Company;
Declarar un parámetro de constructor como privado crea una propiedad interna a la que no se puede acceder desde el código que está dentro de los miembros de la clase a través de la palabra clave this. Si el parámetro no está declarado como público o privado, no se genera propiedad.
La clase debe tener un constructor. Como en C#, si no se le proporciona, se le proporcionará uno. Si la clase amplía otra clase, cualquier constructor que cree debe incluir una llamada a super. Esto llama al constructor en la clase que está ampliando. Este ejemplo incluye un constructor con una llamada superior que proporciona parámetros al constructor de la clase básica:
class MyBaseClass
{
constructor(public x: number, public y: number ) { }
}
class MyDerivedClass extends MyBaseClass
{
constructor()
{
super(2,1);
}
}
TypeScript hereda de otra manera
Una vez más, todo esto le parecerá familiar si es programador de C#, excepto por algunas palabras clave raras (ampliaciones). Sin embargo, nuevamente, al ampliar una clase o una interfaz no es lo mismo que los mecanismos de herencia en C#. La especificación TypeScript usa los términos habituales para la clase que se está ampliando (“clase básica”) y la clase que se amplía (“clase derivada”). Sin embargo, la especificación hace referencia a una “especificación de herencia” de la clase, por ejemplo, en lugar de usar la palabra “herencia”.
Para comenzar, TypeScript cuenta con menos opciones que C# cuando se trata de definir clases básicas. No se puede declarar la clase o los miembros como no reemplazable, abstractos o virtuales (aunque las interfaces proporcionan gran parte de la funcionalidad que ofrece la clase básica virtual).
No hay forma de impedir que algunos miembros no se hereden. Una clase derivada hereda todos los miembros de la clase básica, incluidos los miembros públicos y privados (todos los miembros públicos e la clase básica se reemplazan, mientras que los miembros privados no). Para reemplazar un miembro público, simplemente defina un miembro en la clase derivada con la misma firma. Si bien puede usar la palabra clave super para acceder al método público de una clase derivada, no puede acceder a una propiedad de la clase básica usando super (aunque pueda reemplazar la propiedad).
TypeScript le permite aumentar una interfaz simplemente declarando una interfaz con un nombre idéntico y miembros nuevos. Esto le permite ampliar el código JavaScript existente sin crear un nuevo tipo con nombre. El ejemplo en la Figura 5 define la interfaz ICustomerMerge a través de dos definiciones de interfaz por separado y luego implementa la interfaz en una clase.
Figura 5 La interfaz ICustomerMerge definida a través de dos definiciones de interfaz
interface ICustomerMerge
{
MiddleName: string;
}
interface ICustomerMerge
{
Id: number;
}
class CustomerMerge implements ICustomerMerge
{
Id: number;
MiddleName: string;
}
Las clases también pueden ampliar otras clases, pero no interfaces. En TypeScript, las interfaces también pueden ampliar clases, pero solamente de una forma que implica herencia. Cuando una interfaz amplía una clase, la interfaz incluye todos los miembros de la clase (pública y privada), pero sin las implementaciones de la clase. En la Figura 6, la interfaz ICustomer tendrá el id de miembro privado, el id de miembro público y el MiddleName de nombre público.
Figura 6 Una clase ampliada con todos los miembros
class Customer
{
private id: number;
get Id(): number
{
return this.id
}
set Id( value: number )
{
this.id = value;
}
}
interface ICustomer extends Customer
{
MiddleName: string;
}
La interfaz ICostumer tiene una restricción significativa, solamente puede usarla con clases que amplíen la misma clase que amplió la interfaz (en este caso la clase Customer). TypeScript exige que se incluyan miembros privados en la interfaz que se heredará de la clase que amplía la interfaz, en lugar de que se vuelva a implementar en la clase derivada. Una clase nueva que usa la Interfaz ICustomer necesitaría, por ejemplo, proporciona una implementación para MiddleName (ya que solo se especifica en la interfaz). El programador que use ICustomer podría elegir heredar o reemplazar los métodos públicos de la clase Customer, pero no podría reemplazar el miembro de id privado.
Este ejemplo muestra una clase (llamada NuewCustomer) que implementa la interfaz ICustomer y amplía la clase Customer, según sea necesario. En este ejemplo, NewCustomer hereda la implementación de Id de Customer y proporciona una implementación para MiddleName:
class NewCustomer extends Customer implements ICustomer
{
MiddleName: string;
}
Esta combinación de interfaces, clases implementación y extensión proporciona una manera controlada para las clases que defina de ampliar clases definidas en otros modelos de objeto (para obtener más información, consulte la sección 7.3 de la especificación de idiom “Interfaces que extienden clases”. Junto con la posibilidad de TypeScript de usar información sobre otras bibliotecas de JavaScript, le permite escribir código TypeScript que funciona con los objetos definidos en esas bibliotecas.
TypeScript conoce sobre sus bibliotecas
Además de saber sobre las clases y las interfaces definidas en su aplicación, puede proporcionarle a TypeScript información sobre otras bibliotecas de objetos. Eso se maneja a través de la palabra clave de declaración de TypeScript. Esto crea lo que la especificación llama “declaraciones ambiente”. Es posible que nunca deba usar la palabra clave de declaración porque puede encontrar los archivos de definición para la mayoría de las bibliotecas JavaScript en el sitio DefinitelyTyped en definitelytyped.org. A través de estos archivos de definición, TypeScript puede “leer la documentación” eficazmente sobre las bibliotecas con las cuales debe trabajar.
“Leer la documentación,” por supuesto, significa que se obtiene compatibilidad con IntelliSense de tipos de datos y comprobación de tiempo de compilación al utilizar los objetos que componen la biblioteca. También permite a TypeScript, bajo ciertas circunstancias, inferir el tipo de variable a partir del contexto en el que se utiliza. Gracias a la definición de lib.d.ts incluida con TypeScript, TypeScript da por hecho que el delimitador de variable es de tipo HTMLAnchorElement en el siguiente código:
var anchor = document.createElement( "a" );
El archivo de definición especifica que es el resultado que devuelve el método createElement cuando al método se le pasa la cadena “a”. Saber que el delimitador es un HTMLAnchorElement significa que TypeScript sabe que qué variable respalda, por ejemplo, el método addEventListener.
La interferencia de tipo de datos TypeScript también funciona con tipos de parámetro. Por ejemplo, el método addEventListener acepta dos parámetros. El segundo es una función en la que AddEventListener pasa un objeto de tipo PointerEvent. TypeScript lo sabe y respalda el acceso a la propiedad cancelBubble de la clase PointerEvent dentro de la función:
span.addEventListener("pointerenter", function ( e )
{
e.cancelBubble = true;
}
De la misma manera que lib.d.ts proporciona información sobre HTML DOM, los archivos de definición de otro JavaScript proporcionan una funcionalidad similar. Luego de agregar el archivo backbone.d.ts a mi proyecto, por ejemplo, puedo declarar una clase que extiende la clase Backbone Model e implementa mi propia interfaz con código como este:
class CustomerShort extends bb.Model implements ICustomerShort
{
}
Si está interesado en los detalles de cómo usar TypeScript con Backbone y Knockout, consulte mis columnas Practical TypeScript en bit.ly/1BRh8NJ. El año próximo, examinaré en más detalle el uso de TypeScript con Angular.
Hay mucho más de TypeScript de lo que se ve aquí. TypeScript versión 1.3 está destinado a incluir tipos de datos de unión (para que sean compatibles, por ejemplo, con funciones que devuelvan una lista de tipos específicos) y tuplas. El equipo de TypeScript está trabajando con otros equipos en aplicar tipos de datos a JavaScript (Flow y Angular) para garantizar que TypeScript funcione con una variedad de bibliotecas de JavaScript lo más amplia posible.
Si necesita hacer algo que es compatible con JavaScript pero que TypeScript no le permite hacerlo, siempre puede integrar el código JavaScript ya que TypeScritp es un superconjunto de JavaScript. Por lo tanto nos queda la pregunta, ¿cuáles de estos lenguajes preferiría usar para escribir el código del cliente?
Peter Vogel es director en PH&V Information Services y se especializa en desarrollo web con experiencia en SOA, desarrollo del cliente y diseño de IU. Los clientes de PH&V incluyen Canadian Imperial Bank of Commerce, Volvo y Microsoft. También enseña y dicta cursos para Learning Tree International y escribe la columna Practical .NET para VisualStudioMagazine.com.
Gracias al siguiente experto técnico de Microsoft por revisar este artículo: Ryan Cavanaugh