TypeScript

Equivalencias idiomáticas entre Kotlin y TypeScript. Kotlin se presenta como referencia en cada concepto.

Tipos paramétricos y convenciones del lenguaje

Genéricos, operadores y rangos cambian de sintaxis entre ecosistemas, pero las preguntas de diseño detrás son las mismas: qué varía, qué se comprueba y cuándo.

Equivalencia desde Kotlin

Genéricos y varianza

Aproximada

TypeScript sigue siendo estructuralmente tipado incluso con anotaciones de varianza: in y out documentan y validan la varianza pretendida del parámetro de tipo, pero no vuelven nominal al tipo genérico ni cambian en general el resto de las comparaciones estructurales. La sintaxis de método (accept(value: T): void) retiene una excepción de bivarianza histórica y acepta asignaciones que no son sólidas; una propiedad de tipo función (accept: (value: T) => void) recibe una comprobación contravariante estricta bajo strictFunctionTypes, y rechaza esa misma asignación insegura.

Referencia Kotlin: Source<out T> es covariante y Sink<in T> es contravariante.

Mecanismo
anotaciones de varianza in/out sobre compatibilidad estructural
Idea conservada
Source<out T> es covariante: un Source<Dog> es utilizable donde se requiere un Source<Animal>. Sink<in T> es contravariante: un Sink<Animal> es utilizable donde se requiere un Sink<Dog>.
Riesgo de diseño
Suponer que anotar in/out vuelve nominal la comparación de tipos, o declarar un consumidor con sintaxis de método esperando la misma comprobación estricta que una propiedad de tipo función.
Recomendación
Declara consumidores como propiedades de tipo función cuando strictFunctionTypes deba rechazar asignaciones insalubres; usa sintaxis de método solo cuando la excepción de bivarianza sea intencional.
Convención del curso
No agregues in/out a un genérico salvo que la varianza sea parte del contrato que se enseña.
Mecanismo del lenguaje
Anotaciones in/out en la declaración del parámetro de tipo.

Diferencias relevantes

  • Las anotaciones in/out documentan y validan la varianza pretendida; TypeScript ya infiere la varianza estructuralmente incluso sin ellas en la mayoría de los casos.
  • La excepción de bivarianza solo aplica a miembros declarados con sintaxis de método, no a propiedades de tipo función.

Source<out T> es covariante, Sink<in T> es contravariante.

export interface Animal {
    readonly name: string;
}

export interface Dog extends Animal {
    readonly breed: string;
}

export interface Source<out T> {
    readonly get: () => T;
}

export interface Sink<in T> {
    readonly accept: (value: T) => void;
}

/** Function-valued property: checked contravariantly (strictly) under strictFunctionTypes. */
export interface FunctionPropertySink<T> {
    readonly accept: (value: T) => void;
}

/** The same shape declared with method syntax retains the bivariance exception. */
export interface MethodSink<T> {
    accept(value: T): void;
}

Fuentes

Equivalencia desde Kotlin

Operadores

Aproximada

TypeScript no admite sobrecarga de operadores definida por la persona usuaria: no existe una forma de hacer que value in interval invoque lógica propia. El operador in de JavaScript comprueba pertenencia de propiedades sobre un objeto ("lower" in interval), algo completamente distinto de la pertenencia de rango de Kotlin, así que la pertenencia se expresa siempre con un método explícito como contains.

Referencia Kotlin: operator fun contains permite escribir price in range para una pertenencia definida por el dominio.

Mecanismo
método explícito contains
Idea conservada
ClosedInterval valida que ambos límites sean finitos y que lower <= upper al construirse, y contains(value) expresa la pertenencia explícitamente.
Riesgo de diseño
Esperar que value in interval funcione como el in de Kotlin, cuando en JavaScript comprueba si interval tiene una propiedad llamada value.
Recomendación
Usa siempre interval.contains(value) explícitamente; no intentes emular el operador in de Kotlin.
Convención del curso
La pertenencia siempre se expresa con contains, nunca con el operador in de JavaScript.
Mecanismo del lenguaje
Clase con constructor privado, fábrica of validada y un método contains.

Diferencias relevantes

  • No hay azúcar sintáctico de operador para la pertenencia; siempre se invoca contains explícitamente.
  • El operador in de JavaScript existe, pero comprueba pertenencia de propiedades, no pertenencia de rango.

contains expresa explícitamente la pertenencia que Kotlin resuelve con el operador in.

export class ClosedInterval {
    private constructor(readonly lower: number, readonly upper: number) {}

    static of(lower: number, upper: number): ClosedInterval {
        if (!Number.isFinite(lower) || !Number.isFinite(upper) || lower > upper) {
            throw new RangeError(`Los límites deben ser finitos y lower <= upper, se recibió [${lower}, ${upper}].`);
        }
        return new ClosedInterval(lower, upper);
    }

    contains(value: number): boolean {
        return value >= this.lower && value <= this.upper;
    }
}

Sin objeto invocable: una función simple ya es un valor que se llama con ().

const isAboveMinimum = (price: number): boolean => price >= 10;
isAboveMinimum(79.9);

Fuentes

Equivalencia desde Kotlin

Rangos

Aproximada

closedIntegerRange es un generador perezoso reutilizable para el recorrido discreto de enteros; es una responsabilidad distinta de ClosedInterval (la pertenencia sobre extremos continuos vista en el concepto anterior). Un bucle for...of consume el generador directamente para un recorrido puntual, sin asignar un arreglo intermedio. Array.from(closedIntegerRange(...)) solo tiene sentido cuando el arreglo materializado es realmente el resultado que se necesita, por ejemplo para reutilizarlo varias veces o pasarlo a un método de Array.

Referencia Kotlin: 1..limit representa una progresión de extremos inclusivos.

Mecanismo
generador closedIntegerRange
Idea conservada
closedIntegerRange(start, endInclusive) produce cada entero entre ambos extremos, incluyendo endInclusive exactamente una vez; si start es mayor que endInclusive, produce una secuencia vacía.
Riesgo de diseño
Envolver siempre el generador en Array.from por costumbre, incluso cuando el llamador solo necesita recorrerlo una vez con for...of.
Recomendación
Recorre closedIntegerRange directamente con for...of; usa Array.from solo cuando el resultado deba ser un arreglo materializado.
Convención del curso
Array.from solo cuando el arreglo materializado sea el resultado que se necesita.
Mecanismo del lenguaje
Función generadora (function*) con yield.

Diferencias relevantes

  • closedIntegerRange itera enteros discretos; ClosedInterval solo responde pertenencia sobre extremos continuos, no itera.
  • Un bucle for...of no asigna un arreglo intermedio; Array.from sí, y debe reservarse para cuando ese arreglo es el resultado buscado.

Un generador perezoso para el recorrido discreto de enteros.

export function* closedIntegerRange(start: number, endInclusive: number): Generator<number> {
    for (let value = start; value <= endInclusive; value += 1) {
        yield value;
    }
}

Recorrido directo con for...of; Array.from solo cuando el arreglo materializado es realmente necesario.

for (const value of closedIntegerRange(1, limit)) {
    console.log(value);
}

const materialized = Array.from(closedIntegerRange(1, limit));

Fuentes