TypeScript

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

Modelado de dominio y datos

Cada lenguaje ofrece herramientas distintas para dar forma a un dominio: distinguir qué garantías son del compilador y cuáles son solo convención evita errores de diseño.

Equivalencia desde Kotlin

Clases de valor

Aproximada

TypeScript no tiene un tipo nominal nativo. La validación en tiempo de ejecución la realiza la fábrica con Number.isSafeInteger(remaining) && remaining > 0, que rechaza valores no enteros, no seguros, NaN e Infinity además de los no positivos. La distinción de EvaluationBudget frente a number existe solo en tiempo de compilación, mediante una marca (unique symbol) que el verificador de tipos usa para impedir mezclarlos. Esa marca se borra por completo al compilar: en tiempo de ejecución, EvaluationBudget es un number sin ningún metadato adicional.

Referencia Kotlin: Declarar un tipo nominal para un dato primitivo y validar su invariante al construirse.

Mecanismo
Tipo marcado (brand) con un unique symbol y una función fábrica que valida antes de construir.
Idea conservada
El tipo distingue EvaluationBudget de number en tiempo de compilación, y la fábrica rechaza en tiempo de ejecución cualquier valor que no sea un entero seguro y positivo.
Riesgo de diseño
Hacer un cast directo (as EvaluationBudget) de un number a EvaluationBudget sin pasar por la fábrica, saltándose la validación de entero seguro y positivo.
Recomendación
Expón solo la fábrica y el tipo marcado; no exportes una forma de construir el valor sin validar.
Convención del curso
Nunca exportes una conversión directa number -> EvaluationBudget; solo la fábrica validada.
Mecanismo del lenguaje
Tipos marcados (brand) mediante un unique symbol no exportado.

Diferencias relevantes

  • La marca es solo de tipos: se borra por completo al compilar, así que en tiempo de ejecución EvaluationBudget es indistinguible de un number.
  • Nada impide construir el valor sin pasar por la fábrica salvo la disciplina del equipo, ya que un as EvaluationBudget explícito puede saltarse la validación.

La fábrica es la única forma pública de obtener un EvaluationBudget.

Kotlin
@JvmInline
value class EvaluationBudget(val remaining: Int) {
    init {
        require(remaining > 0) { "El presupuesto debe ser positivo." }
    }
}

val budget = EvaluationBudget(1000) // remaining = 1000
TypeScript
declare const evaluationBudgetBrand: unique symbol;

/** A positive, safe-integer count of evaluations remaining in an optimization budget. */
export type EvaluationBudget = number & { readonly [evaluationBudgetBrand]: true };

export function evaluationBudget(remaining: number): EvaluationBudget {
    if (!Number.isSafeInteger(remaining) || remaining <= 0) {
        throw new RangeError(`El presupuesto debe ser un entero seguro positivo, se recibió ${remaining}.`);
    }
    return remaining as EvaluationBudget;
}

Fuentes

Equivalencia desde Kotlin

Construcción controlada

Aproximada

Esta página usa Delta —una diferencia numérica finita y positiva— como equivalente TypeScript del fixture compartido de edad no negativa; el vocabulario de dominio difiere porque Delta también cubre el caso no finito que el fixture de Kotlin no ejercita. private constructor impide invocar new Delta directamente desde código TypeScript type-checked: el verificador de tipos rechaza la llamada. No es una frontera de seguridad de JavaScript en tiempo de ejecución, ya que el código compilado a JavaScript ya no tiene ninguna noción de visibilidad.

Referencia Kotlin: Un constructor privado y una fábrica validan el invariante.

Mecanismo
clase con constructor privado y una fábrica estática of
Idea conservada
La única forma type-checked de obtener un Delta es a través de Delta.of, que valida que el valor sea finito y positivo antes de construir la instancia.
Riesgo de diseño
Describir private constructor como una barrera de seguridad en tiempo de ejecución, cuando solo el verificador de tipos la aplica antes de compilar.
Recomendación
Expón únicamente Delta.of como forma de construcción y documenta que la privacidad es una garantía de tiempo de compilación, no de ejecución.
Convención del curso
Nunca expongas el constructor; solo la fábrica of.
Mecanismo del lenguaje
Constructor privado más una fábrica estática validada.

Diferencias relevantes

  • private aquí es una restricción de tiempo de compilación aplicada por el verificador de tipos, no una garantía del runtime de JavaScript.
  • Delta.of lanza un RangeError en vez de devolver un tipo opcional o un Result, a diferencia de otras fábricas de este mismo concepto en otros ecosistemas.

Delta.of es la única forma type-checked de obtener un Delta.

export class Delta {
    private constructor(readonly value: number) {}

    static of(value: number): Delta {
        if (!Number.isFinite(value) || value <= 0) {
            throw new RangeError(`El delta debe ser un valor finito positivo, se recibió ${value}.`);
        }
        return new Delta(value);
    }
}

Fuentes

Equivalencia desde Kotlin

Clases de datos

Aproximada

Un type alias no genera ninguna operación de valor: ni igualdad, ni hash, ni copia, ni una representación en cadena legible. Dos Measurement con los mismos campos son objetos distintos, así que === compara identidad de referencia, no sus datos. measurementsEqual declara explícitamente qué significa que dos mediciones sean iguales para este dominio. Readonly<T> afecta la asignabilidad en tiempo de compilación —el verificador de tipos rechaza reasignar name o value— pero es superficial y no congela el objeto en tiempo de ejecución.

Referencia Kotlin: data class genera operaciones de valor conocidas.

Mecanismo
type alias sobre un objeto, con igualdad definida explícitamente por la aplicación
Idea conservada
measurementsEqual compara ambos campos explícitamente, usando Object.is para el campo numérico para tener un comportamiento bien definido ante NaN y cero con signo.
Riesgo de diseño
Comparar dos valores Measurement con === esperando igualdad por datos, o suponer que Readonly<T> congela el objeto en tiempo de ejecución.
Recomendación
Declara measurementsEqual junto al tipo y úsala siempre que el dominio necesite comparar mediciones por sus datos.
Convención del curso
Toda comparación de mediciones por datos pasa por measurementsEqual, nunca por ===.
Mecanismo del lenguaje
Type alias con Readonly<T> más una función de igualdad explícita.

Diferencias relevantes

  • === compara identidad de referencia; dos Measurement con los mismos datos no son === entre sí.
  • Readonly<T> es una restricción de asignabilidad en tiempo de compilación, superficial y no equivalente a Object.freeze en tiempo de ejecución.

measurementsEqual declara explícitamente la igualdad por datos que el type alias no provee.

export type Measurement = Readonly<{
    name: string;
    value: number;
}>;

export function measurementsEqual(left: Measurement, right: Measurement): boolean {
    return left.name === right.name && Object.is(left.value, right.value);
}

Fuentes

Equivalencia desde Kotlin

Singletons

Aproximada

Un binding exportado a nivel de módulo se comparte dentro de una instancia evaluada concreta del módulo, no de manera incondicional: un bundler, un worker o un grafo de dependencias duplicado puede evaluar el módulo más de una vez, produciendo instancias separadas. as const marca las propiedades del objeto como de solo lectura en tiempo de compilación —Object.isFrozen devuelve false sobre ese objeto— y no congela nada en tiempo de ejecución. Object.freeze es el mecanismo separado y explícito para obtener inmutabilidad real en tiempo de ejecución.

Referencia Kotlin: object declara una instancia única del lenguaje.

Mecanismo
instancia compartida a nivel de módulo
Idea conservada
defaultSettings y frozenSettings exponen ambos mecanismos por separado: el primero solo restringe la asignabilidad en tiempo de compilación, el segundo también congela en tiempo de ejecución.
Riesgo de diseño
Afirmar que as const congela el objeto en tiempo de ejecución, cuando Object.isFrozen sobre ese objeto devuelve false.
Recomendación
Usa as const para expresar de solo lectura en tiempo de compilación; añade Object.freeze explícitamente solo cuando el dominio necesite esa garantía también en tiempo de ejecución.
Convención del curso
No llames 'singleton' a un binding de módulo sin calificar el alcance de esa unicidad.
Mecanismo del lenguaje
Binding exportado a nivel de módulo, opcionalmente combinado con Object.freeze.

Diferencias relevantes

  • as const no llama a Object.freeze; ambos deben combinarse explícitamente si se necesitan las dos garantías.
  • La unicidad de un binding de módulo es por instancia de módulo evaluada, no una garantía incondicional como la de un object de Kotlin dentro de un mismo classloader.

as const y Object.freeze son mecanismos separados con garantías distintas.

export const defaultSettings = {
    retries: 3,
} as const;

export const frozenSettings = Object.freeze({
    retries: 3,
});

Fuentes

Equivalencia desde Kotlin

Enumeraciones

Aproximada

type Status = "new" | "done" es una unión literal: un tipo cerrado de valores de cadena, sin discriminante ni objeto en tiempo de ejecución. Cuando además de comprobar tipos se necesitan los nombres en tiempo de ejecución (por ejemplo para iterar o serializar), un objeto as const junto a un tipo derivado con typeof ... [keyof typeof ...] expresa lo mismo sin duplicar los literales. TypeScript también tiene un enum nativo, que sí emite un objeto propio en tiempo de ejecución; solo numeric enum recibe mapeo inverso automático. Vale la pena cuando una API o base de código existente ya espera ese objeto generado por el compilador.

Referencia Kotlin: enum class declara constantes nombradas y puede contener datos o comportamiento.

Mecanismo
unión literal
Idea conservada
Los valores de StatusValues coinciden exactamente con los miembros de la unión literal Status.
Riesgo de diseño
Llamar 'unión discriminada' a una unión literal sin campo discriminante, o introducir un enum nativo solo para tener nombres en tiempo de ejecución cuando un objeto as const ya lo resuelve.
Recomendación
Prefiere una unión literal cuando solo se necesita un conjunto cerrado de valores; añade un objeto as const cuando los nombres también sean útiles en tiempo de ejecución.
Convención del curso
No llames discriminada a una unión literal sin campo discriminante.
Mecanismo del lenguaje
Unión literal, con un objeto as const opcional para uso en tiempo de ejecución.

Diferencias relevantes

  • Una unión literal no tiene representación en tiempo de ejecución; un enum nativo sí emite un objeto.
  • Solo un enum numérico recibe mapeo inverso automático; no es una razón general para preferir enum sobre una unión literal.

Una unión literal, su forma as const con nombres en tiempo de ejecución, y el tipo derivado.

export type Status = "new" | "done";

export const StatusValues = {
    New: "new",
    Done: "done",
} as const;

export type DerivedStatus = (typeof StatusValues)[keyof typeof StatusValues];

Fuentes

Equivalencia desde Kotlin

Jerarquías selladas

Aproximada

Una unión discriminada modela SearchResult como un tipo cerrado con dos formas posibles. Sin embargo, TypeScript no comprueba la exhaustividad del switch por sí solo: solo lo hace si se agrega una rama default que exige el tipo never.

Referencia Kotlin: Declarar una familia cerrada de alternativas para que el compilador exija manejar cada caso.

Mecanismo
Unión discriminada por una propiedad kind y un switch sobre ese discriminante.
Idea conservada
Cada variante es un caso cerrado con su propio payload; el discriminante kind identifica cuál caso es en tiempo de ejecución.
Riesgo de diseño
Omitir una rama default con una comprobación de never y asumir que TypeScript avisará si falta un caso; no lo hace por defecto.
Recomendación
Agrega una rama default que reciba el resultado como never para forzar un error de compilación cuando aparezca un caso nuevo.
Convención del curso
Fuerza la exhaustividad con una rama default tipada como never.
Mecanismo del lenguaje
Uniones discriminadas.

Diferencias relevantes

  • TypeScript no obliga a manejar todos los casos por sí solo: la exhaustividad solo se comprueba si se agrega una rama default que exige el tipo never.
  • La unión es estructural: cualquier objeto con la forma correcta pertenece al tipo, no solo instancias construidas explícitamente.

La rama default fuerza la exhaustividad mediante el tipo never.

Kotlin
sealed interface SearchResult {
    data class Completed(val bestValue: Double) : SearchResult
    data class Failed(val reason: String) : SearchResult
}

fun describe(result: SearchResult): String = when (result) {
    is SearchResult.Completed -> "Completed(${result.bestValue})"
    is SearchResult.Failed -> "Failed(${result.reason})"
}
TypeScript
type SearchResult =
    | { kind: "completed"; bestValue: number }
    | { kind: "failed"; reason: string };

function describe(result: SearchResult): string {
    switch (result.kind) {
        case "completed":
            return `Completed(${result.bestValue})`;
        case "failed":
            return `Failed(${result.reason})`;
        default: {
            const exhaustive: never = result;
            throw new Error(`Unhandled case: ${exhaustive}`);
        }
    }
}

Fuentes

Equivalencia desde Kotlin

Alias de tipos

Cercana

En TypeScript, type permite implementar este comportamiento: Da un nombre legible a una firma de transformación.

Referencia Kotlin: typealias crea un nombre alternativo, no un nuevo tipo nominal.

Mecanismo
type
Riesgo de diseño
Confundir la forma de type con una garantía que TypeScript no ofrece en tiempo de ejecución.
Recomendación
Declara el contrato observable y aplica type solo a la garantía que proporciona.

Diferencias relevantes

  • No presentes un alias como una frontera de dominio cuando no cambia la identidad de tipo.
  • La comprobación estática guía el diseño, pero el programa conserva la semántica de JavaScript al ejecutarse.

Da un nombre legible a una firma de transformación.

type Transform = (value: number) => number;

Fuentes