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!