Manuel Rueda Blog

TypeScript Tipos Complejos

April 03, 2019 - 3 minutes read

    Otros idiomas:
  • English 🇺🇸

El sistema de tipos de TypeScript es Turing Completo y eso nos da el poder de definir tipos complejos para alimentar nuestro código. Esto es posible gracias a tipos mapeados (mapped types), tipos recursivos (recursive types), tipos condicionales (conditional types), tipos accesibles por indice (index accesibles types), union de tipos(union types) y tipos genéricos (generic types).

Esto es genial, pero no hay mucha documentación o ejemplos prácticos al respecto, el articulo TypeScript Advanced Types tiene algunos ejemplos y explicaciones mínimas acerca de como usarlos.

En este articulo les voy a mostrar ejemplos “de la vida real” para demostrar que se puede obtener con esto.

Tipo de auto-referencia

Digamos que quiero crear un objeto que tiene una propiedad foo de tipo string y a una propiedad bar la cual tendra su tipo definido en base al valor de foo.

interface FooTypesMap {
    one: string;
    two: boolean;
    three: {
        one: Date
    };
}

type MyType = {
    [k in keyof FooTypesMap]: {
        foo: k,
        bar: FooTypesMap[k],
    }
}[keyof FooTypesMap]


const instanceOne: MyType = {
    foo: 'one',
    bar: 'algún valor',
}

const instanceTwo: MyType = {
    foo: 'two',
    bar: true,
}

const instanceThree: MyType = {
    foo: 'three',
    bar: {
        one: new Date()
    },
}

En este ejemplo estamos usando tipos mapeados (mapped types), tipos accesibles por indice (index accesibles types) y union de tipos(union types) para obtener un tipo que cambia en base al valor de una propiedad de la instancia.

Tipo de auto-referencia especifico

Si juegan un poco con el ejemplo anterior ser darán cuenta que MyType es una union de tipos(union types) de todos las posibles combinaciones y cuando el valor de foo es definido, TypeScript puede reducir el numero de posibilidades a solo las que respetan foo === X. Pero como hacemos si queremos declarar una variable con una de esas posibilidades específicamente? Eso no es posible con este ejemplo pero se puede obtener con el siguiente.

interface FooTypesMap {
    never: never;
    one: string;
    two: boolean;
    three: {
        one: Date
    };
}

type MyType<TType extends keyof FooTypesMap = 'never'> = {
    [k in keyof FooTypesMap]: {
        foo: TType extends 'never' ? k : TType,
        bar: FooTypesMap[TType extends 'never' ? k : TType],
    }
}[keyof FooTypesMap]

let instanceFour: MyType<'three'>;

instanceFour = {
    foo: 'three',
    bar: {
        one: new Date()
    }
}

La diferencia con el ejemplo aterior es que agregamos un nuevo tipo genérico opcional (optional generic type) con un valor predeterminado never y es usado dentro de un tipo condicional (conditional type) para verificar si fue declarado o usa el valor predeterminado. Si esta declarado se usara ese tipo para definir el tipo de foo y con ese se obtendrá el valor de bar y si no esa definido se usara k como el ejemplo anterior.

Lo único raro en este código es la definición never: never es FooTypesMap, esto nos permite mantener la restricción de tipo TType extends keyof FooTypesMap que define que TType tiene que ser una propiedad en FooTypesMap.

Tipo de auto-referencia recursivo

Hay un nivel mas de complejidad que podemos agregar a MyType, que pasas si queremos una propiedad extra llamada baz que sea de tipo MyType. Para hacer esto debemos usa tipos recursivos (recursive types).

Ademas quiero agregar una propiedad llamada qux que sea un mapa con N cantidad de propiedades donde todas deben ser de tipo MyType, para mostrar el podes de los tipos tipos recursivos (recursive types).

Nótese que probablemente querrán hacer opcionales las propiedades de tipo recursivo porque sino pueden termina con una cadena infinita de declaraciones.

interface FooTypesMap {
    never: never;
    one: string;
    two: boolean;
    three: {
        one: Date
    };
}

type MyType<TType extends keyof FooTypesMap = 'never'> = {
    [k in keyof FooTypesMap]: {
        foo: TType extends 'never' ? k : TType,
        bar: FooTypesMap[TType extends 'never' ? k : TType],
        baz?: MyType,
        qux: {
            [key: string]: MyType
        }
    }
}[keyof FooTypesMap]

let instance: MyType<'three'>;

instance = {
    foo: 'three',
    bar: {
        one: new Date()
    },
    baz: {
        foo: 'one',
        bar: 'algún valor'
    },
    qux: {
        myKey: {
            foo: 'two',
            bar: false,
        }
    }
}

Gracias Keshav por la ayuda con los tipos!


Manuel Rueda

My name is Manuel Rueda and welcome to my blog. You can also follow me on Twitter and/or Github.