Capítulo 2: TypeScript en React Native
Capítulo 2: TypeScript en React Native
En este capítulo profundizaremos en el uso profesional de TypeScript en React Native, creando tipos robustos y reutilizables para nuestra aplicación TODO.
🎯 Objetivos del Capítulo
Al finalizar este capítulo serás capaz de:
- ✅ Configurar TypeScript de forma avanzada para React Native
- ✅ Crear tipos e interfaces para componentes
- ✅ Tipar correctamente props, state y eventos
- ✅ Implementar tipos para navegación y rutas
- ✅ Usar tipos utilitarios de TypeScript
📋 Configuración Avanzada de TypeScript
Paso 1: Tipos Globales para React Native
Crea el archivo src/types/global.d.ts:
// src/types/global.d.ts
// Extensiones de tipos para React Native
declare module '*.png' {
const value: any;
export = value;
}
declare module '*.jpg' {
const value: any;
export = value;
}
declare module '*.svg' {
const value: any;
export = value;
}
// Variables de entorno
declare module '@env' {
export const API_URL: string;
export const CLERK_PUBLISHABLE_KEY: string;
export const SENTRY_DSN: string;
}
// Extensión de tipos de React Native
declare module 'react-native' {
interface PressableStateCallbackType {
pressed: boolean;
}
interface ViewStyle {
elevation?: number;
}
}
Paso 2: Configuración de Path Mapping
Actualiza babel.config.js para soporte de path mapping:
// babel.config.js
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: [
[
'module-resolver',
{
root: ['./src'],
alias: {
'@': './src',
'@/components': './src/components',
'@/screens': './src/screens',
'@/stores': './src/stores',
'@/types': './src/types',
'@/utils': './src/utils',
'@/services': './src/services',
'@/hooks': './src/hooks',
},
},
],
],
};
};
Instala el plugin necesario:
npm install --save-dev babel-plugin-module-resolver
🏗️ Tipos Base para la Aplicación
Paso 1: Tipos de Datos Core
Crea src/types/todo.ts:
// src/types/todo.ts
export interface Todo {
id: string;
title: string;
description?: string;
completed: boolean;
priority: Priority;
category: Category;
dueDate?: Date;
createdAt: Date;
updatedAt: Date;
userId: string;
tags: string[];
}
export type Priority = 'low' | 'medium' | 'high' | 'urgent';
export interface Category {
id: string;
name: string;
color: string;
icon: string;
userId: string;
}
export interface CreateTodoInput {
title: string;
description?: string;
priority: Priority;
categoryId: string;
dueDate?: Date;
tags?: string[];
}
export interface UpdateTodoInput extends Partial<CreateTodoInput> {
id: string;
completed?: boolean;
}
export interface TodoFilters {
completed?: boolean;
priority?: Priority[];
categoryId?: string;
search?: string;
dateRange?: {
start: Date;
end: Date;
};
tags?: string[];
}
export interface TodoStats {
total: number;
completed: number;
pending: number;
overdue: number;
byPriority: Record<Priority, number>;
byCategory: Record<string, number>;
}
// Tipos para formularios
export interface TodoFormData {
title: string;
description: string;
priority: Priority;
categoryId: string;
dueDate: string; // ISO string para formularios
tags: string;
}
export interface TodoFormErrors {
title?: string;
description?: string;
priority?: string;
categoryId?: string;
dueDate?: string;
tags?: string;
}
Paso 2: Tipos de Usuario y Autenticación
Crea src/types/auth.ts:
// src/types/auth.ts
export interface User {
id: string;
email: string;
firstName?: string;
lastName?: string;
imageUrl?: string;
username?: string;
createdAt: Date;
lastSignInAt?: Date;
}
export interface AuthState {
user: User | null;
isLoaded: boolean;
isSignedIn: boolean;
isLoading: boolean;
}
export interface SignInData {
email: string;
password: string;
}
export interface SignUpData {
email: string;
password: string;
firstName?: string;
lastName?: string;
}
export interface AuthError {
code: string;
message: string;
field?: string;
}
// Tipos para Clerk
export interface ClerkUser {
id: string;
emailAddresses: Array<{
emailAddress: string;
id: string;
}>;
firstName: string | null;
lastName: string | null;
imageUrl: string;
username: string | null;
createdAt: number;
lastSignInAt: number | null;
}
Paso 3: Tipos de API y Respuestas
Crea src/types/api.ts:
// src/types/api.ts
export interface ApiResponse<T = any> {
data: T;
message: string;
success: boolean;
}
export interface ApiError {
message: string;
code: string;
status: number;
errors?: Record<string, string[]>;
}
export interface PaginationParams {
page: number;
limit: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
}
export interface PaginatedResponse<T> {
data: T[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
};
}
// Tipos para TanStack Query
export interface QueryOptions {
enabled?: boolean;
staleTime?: number;
cacheTime?: number;
refetchOnWindowFocus?: boolean;
retry?: boolean | number;
}
export interface MutationOptions<TData, TError, TVariables> {
onSuccess?: (data: TData) => void;
onError?: (error: TError) => void;
onSettled?: (data: TData | undefined, error: TError | null) => void;
}
🎨 Tipado de Componentes
Paso 1: Tipos para Props de Componentes
Crea src/types/components.ts:
// src/types/components.ts
import { ReactNode } from 'react';
import { ViewStyle, TextStyle, ImageStyle, PressableProps } from 'react-native';
// Tipos base para estilos
export type Style = ViewStyle | TextStyle | ImageStyle;
// Props base para componentes
export interface BaseComponentProps {
testID?: string;
style?: Style;
children?: ReactNode;
}
// Tipos para botones
export interface ButtonProps extends BaseComponentProps, Omit<PressableProps, 'style'> {
title: string;
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
size?: 'small' | 'medium' | 'large';
loading?: boolean;
disabled?: boolean;
icon?: ReactNode;
iconPosition?: 'left' | 'right';
fullWidth?: boolean;
}
// Tipos para inputs
export interface InputProps extends BaseComponentProps {
label?: string;
placeholder?: string;
value: string;
onChangeText: (text: string) => void;
error?: string;
disabled?: boolean;
secureTextEntry?: boolean;
multiline?: boolean;
numberOfLines?: number;
maxLength?: number;
keyboardType?: 'default' | 'numeric' | 'email-address' | 'phone-pad';
autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters';
autoCorrect?: boolean;
leftIcon?: ReactNode;
rightIcon?: ReactNode;
}
// Tipos para modales
export interface ModalProps extends BaseComponentProps {
visible: boolean;
onClose: () => void;
title?: string;
size?: 'small' | 'medium' | 'large' | 'fullscreen';
animationType?: 'slide' | 'fade' | 'none';
transparent?: boolean;
onShow?: () => void;
onDismiss?: () => void;
}
// Tipos para listas
export interface ListItemProps<T = any> extends BaseComponentProps {
item: T;
index: number;
onPress?: (item: T) => void;
onLongPress?: (item: T) => void;
selected?: boolean;
disabled?: boolean;
}
export interface ListProps<T = any> extends BaseComponentProps {
data: T[];
renderItem: (props: ListItemProps<T>) => ReactNode;
keyExtractor?: (item: T, index: number) => string;
loading?: boolean;
error?: string;
emptyMessage?: string;
onRefresh?: () => void;
refreshing?: boolean;
onEndReached?: () => void;
onEndReachedThreshold?: number;
}
Paso 2: Componente Button Tipado
Crea src/components/ui/Button.tsx:
// src/components/ui/Button.tsx
import React from 'react';
import {
Pressable,
Text,
StyleSheet,
ActivityIndicator,
View,
} from 'react-native';
import { ButtonProps } from '@/types/components';
export const Button: React.FC<ButtonProps> = ({
title,
variant = 'primary',
size = 'medium',
loading = false,
disabled = false,
icon,
iconPosition = 'left',
fullWidth = false,
style,
testID,
onPress,
...pressableProps
}) => {
const isDisabled = disabled || loading;
const buttonStyles = [
styles.base,
styles[variant],
styles[`size_${size}`],
fullWidth && styles.fullWidth,
isDisabled && styles.disabled,
style,
];
const textStyles = [
styles.text,
styles[`text_${variant}`],
styles[`textSize_${size}`],
isDisabled && styles.textDisabled,
];
const renderContent = () => {
if (loading) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator
size="small"
color={variant === 'primary' ? '#fff' : '#007AFF'}
/>
<Text style={[textStyles, styles.loadingText]}>{title}</Text>
</View>
);
}
if (icon) {
return (
<View style={[
styles.contentContainer,
iconPosition === 'right' && styles.contentReverse
]}>
{icon}
<Text style={textStyles}>{title}</Text>
</View>
);
}
return <Text style={textStyles}>{title}</Text>;
};
return (
<Pressable
style={({ pressed }) => [
...buttonStyles,
pressed && !isDisabled && styles.pressed,
]}
disabled={isDisabled}
onPress={onPress}
testID={testID}
{...pressableProps}
>
{renderContent()}
</Pressable>
);
};
const styles = StyleSheet.create({
base: {
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
},
// Variantes
primary: {
backgroundColor: '#007AFF',
},
secondary: {
backgroundColor: '#F2F2F7',
},
outline: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: '#007AFF',
},
ghost: {
backgroundColor: 'transparent',
},
// Tamaños
size_small: {
paddingHorizontal: 12,
paddingVertical: 8,
minHeight: 32,
},
size_medium: {
paddingHorizontal: 16,
paddingVertical: 12,
minHeight: 44,
},
size_large: {
paddingHorizontal: 20,
paddingVertical: 16,
minHeight: 56,
},
// Estados
disabled: {
opacity: 0.5,
},
pressed: {
opacity: 0.8,
},
fullWidth: {
width: '100%',
},
// Texto
text: {
fontWeight: '600',
textAlign: 'center',
},
text_primary: {
color: '#fff',
},
text_secondary: {
color: '#007AFF',
},
text_outline: {
color: '#007AFF',
},
text_ghost: {
color: '#007AFF',
},
textSize_small: {
fontSize: 14,
},
textSize_medium: {
fontSize: 16,
},
textSize_large: {
fontSize: 18,
},
textDisabled: {
opacity: 0.7,
},
// Contenedores
contentContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
contentReverse: {
flexDirection: 'row-reverse',
},
loadingContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
loadingText: {
opacity: 0.8,
},
});
Paso 3: Componente Input Tipado
Crea src/components/ui/Input.tsx:
// src/components/ui/Input.tsx
import React, { useState } from 'react';
import {
View,
Text,
TextInput,
StyleSheet,
Pressable,
} from 'react-native';
import { InputProps } from '@/types/components';
export const Input: React.FC<InputProps> = ({
label,
placeholder,
value,
onChangeText,
error,
disabled = false,
secureTextEntry = false,
multiline = false,
numberOfLines = 1,
maxLength,
keyboardType = 'default',
autoCapitalize = 'sentences',
autoCorrect = true,
leftIcon,
rightIcon,
style,
testID,
}) => {
const [isFocused, setIsFocused] = useState(false);
const [isSecure, setIsSecure] = useState(secureTextEntry);
const containerStyles = [
styles.container,
style,
];
const inputContainerStyles = [
styles.inputContainer,
isFocused && styles.inputContainerFocused,
error && styles.inputContainerError,
disabled && styles.inputContainerDisabled,
];
const inputStyles = [
styles.input,
multiline && styles.inputMultiline,
leftIcon && styles.inputWithLeftIcon,
rightIcon && styles.inputWithRightIcon,
];
const toggleSecureEntry = () => {
setIsSecure(!isSecure);
};
return (
<View style={containerStyles}>
{label && (
<Text style={[styles.label, error && styles.labelError]}>
{label}
</Text>
)}
<View style={inputContainerStyles}>
{leftIcon && (
<View style={styles.leftIconContainer}>
{leftIcon}
</View>
)}
<TextInput
style={inputStyles}
placeholder={placeholder}
placeholderTextColor="#999"
value={value}
onChangeText={onChangeText}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
editable={!disabled}
secureTextEntry={isSecure}
multiline={multiline}
numberOfLines={numberOfLines}
maxLength={maxLength}
keyboardType={keyboardType}
autoCapitalize={autoCapitalize}
autoCorrect={autoCorrect}
testID={testID}
/>
{secureTextEntry && (
<Pressable
onPress={toggleSecureEntry}
style={styles.rightIconContainer}
>
<Text style={styles.toggleText}>
{isSecure ? '👁️' : '🙈'}
</Text>
</Pressable>
)}
{rightIcon && !secureTextEntry && (
<View style={styles.rightIconContainer}>
{rightIcon}
</View>
)}
</View>
{error && (
<Text style={styles.errorText}>
{error}
</Text>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
marginBottom: 16,
},
label: {
fontSize: 16,
fontWeight: '600',
color: '#333',
marginBottom: 8,
},
labelError: {
color: '#FF3B30',
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderColor: '#E5E5EA',
borderRadius: 8,
backgroundColor: '#fff',
minHeight: 44,
},
inputContainerFocused: {
borderColor: '#007AFF',
shadowColor: '#007AFF',
shadowOffset: {
width: 0,
height: 0,
},
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 2,
},
inputContainerError: {
borderColor: '#FF3B30',
},
inputContainerDisabled: {
backgroundColor: '#F2F2F7',
opacity: 0.6,
},
input: {
flex: 1,
paddingHorizontal: 12,
paddingVertical: 12,
fontSize: 16,
color: '#333',
},
inputMultiline: {
paddingTop: 12,
textAlignVertical: 'top',
},
inputWithLeftIcon: {
paddingLeft: 8,
},
inputWithRightIcon: {
paddingRight: 8,
},
leftIconContainer: {
paddingLeft: 12,
paddingRight: 8,
},
rightIconContainer: {
paddingRight: 12,
paddingLeft: 8,
},
toggleText: {
fontSize: 16,
},
errorText: {
fontSize: 14,
color: '#FF3B30',
marginTop: 4,
},
});
🧭 Tipado de Navegación
Paso 1: Tipos de Navegación
Crea src/types/navigation.ts:
// src/types/navigation.ts
import { NavigatorScreenParams } from '@react-navigation/native';
import { StackScreenProps } from '@react-navigation/stack';
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
import { Todo } from './todo';
// Root Stack Navigator
export type RootStackParamList = {
Auth: NavigatorScreenParams<AuthStackParamList>;
Main: NavigatorScreenParams<MainTabParamList>;
TodoDetail: { todoId: string };
TodoForm: { todoId?: string; mode: 'create' | 'edit' };
Settings: undefined;
Profile: undefined;
};
// Auth Stack Navigator
export type AuthStackParamList = {
Welcome: undefined;
SignIn: undefined;
SignUp: undefined;
ForgotPassword: undefined;
};
// Main Tab Navigator
export type MainTabParamList = {
Home: undefined;
TodoList: NavigatorScreenParams<TodoStackParamList>;
Categories: undefined;
Stats: undefined;
};
// Todo Stack Navigator
export type TodoStackParamList = {
TodoListScreen: undefined;
TodoSearch: undefined;
TodoFilters: undefined;
};
// Screen Props Types
export type RootStackScreenProps<T extends keyof RootStackParamList> =
StackScreenProps<RootStackParamList, T>;
export type AuthStackScreenProps<T extends keyof AuthStackParamList> =
StackScreenProps<AuthStackParamList, T>;
export type MainTabScreenProps<T extends keyof MainTabParamList> =
BottomTabScreenProps<MainTabParamList, T>;
export type TodoStackScreenProps<T extends keyof TodoStackParamList> =
StackScreenProps<TodoStackParamList, T>;
// Navigation Props
export interface NavigationProps {
navigation: any; // Se puede tipar más específicamente según el contexto
route: any;
}
// Deep Linking
export interface DeepLinkConfig {
screens: {
Auth: {
screens: {
SignIn: 'signin';
SignUp: 'signup';
};
};
Main: {
screens: {
Home: 'home';
TodoList: 'todos';
Categories: 'categories';
Stats: 'stats';
};
};
TodoDetail: 'todo/:todoId';
TodoForm: 'todo/form/:mode/:todoId?';
};
}
Paso 2: Hook de Navegación Tipado
Crea src/hooks/useTypedNavigation.ts:
// src/hooks/useTypedNavigation.ts
import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { RootStackParamList } from '@/types/navigation';
export type NavigationProp = StackNavigationProp<RootStackParamList>;
export const useTypedNavigation = () => {
return useNavigation<NavigationProp>();
};
// Hook para rutas tipadas
export const useTypedRoute = <T extends keyof RootStackParamList>() => {
const route = useRoute<RouteProp<RootStackParamList, T>>();
return route;
};
🛠️ Utilidades de TypeScript
Paso 1: Tipos Utilitarios
Crea src/types/utils.ts:
// src/types/utils.ts
// Hacer todas las propiedades opcionales excepto las especificadas
export type PartialExcept<T, K extends keyof T> = Partial<T> & Pick<T, K>;
// Hacer todas las propiedades requeridas excepto las especificadas
export type RequiredExcept<T, K extends keyof T> = Required<T> & Partial<Pick<T, K>>;
// Extraer tipos de arrays
export type ArrayElement<T> = T extends (infer U)[] ? U : never;
// Tipo para valores de enum
export type EnumValues<T> = T[keyof T];
// Tipo para funciones async
export type AsyncReturnType<T extends (...args: any) => Promise<any>> =
T extends (...args: any) => Promise<infer R> ? R : any;
// Tipo para manejar estados de loading
export interface LoadingState<T = any> {
data: T | null;
loading: boolean;
error: string | null;
}
// Tipo para respuestas de API con estado
export interface ApiState<T = any> extends LoadingState<T> {
refetch: () => void;
refresh: () => void;
}
// Tipo para formularios
export interface FormState<T = any> {
values: T;
errors: Partial<Record<keyof T, string>>;
touched: Partial<Record<keyof T, boolean>>;
isValid: boolean;
isSubmitting: boolean;
}
// Tipo para modales
export interface ModalState {
visible: boolean;
data?: any;
}
// Tipo para filtros
export type FilterValue<T> = T | T[] | undefined;
// Tipo para ordenamiento
export interface SortConfig<T> {
key: keyof T;
direction: 'asc' | 'desc';
}
// Tipo para paginación
export interface PaginationState {
page: number;
limit: number;
total: number;
hasNext: boolean;
hasPrev: boolean;
}
Paso 2: Validadores con TypeScript
Crea src/utils/validators.ts:
// src/utils/validators.ts
import { TodoFormData, TodoFormErrors } from '@/types/todo';
export type ValidationRule<T> = {
required?: boolean;
minLength?: number;
maxLength?: number;
pattern?: RegExp;
custom?: (value: T) => string | undefined;
};
export type ValidationSchema<T> = {
[K in keyof T]?: ValidationRule<T[K]>;
};
export const validateField = <T>(
value: T,
rules: ValidationRule<T>
): string | undefined => {
if (rules.required && (!value || (typeof value === 'string' && !value.trim()))) {
return 'Este campo es requerido';
}
if (typeof value === 'string') {
if (rules.minLength && value.length < rules.minLength) {
return `Debe tener al menos ${rules.minLength} caracteres`;
}
if (rules.maxLength && value.length > rules.maxLength) {
return `No debe exceder ${rules.maxLength} caracteres`;
}
if (rules.pattern && !rules.pattern.test(value)) {
return 'Formato inválido';
}
}
if (rules.custom) {
return rules.custom(value);
}
return undefined;
};
export const validateForm = <T extends Record<string, any>>(
data: T,
schema: ValidationSchema<T>
): Partial<Record<keyof T, string>> => {
const errors: Partial<Record<keyof T, string>> = {};
for (const field in schema) {
const rules = schema[field];
if (rules) {
const error = validateField(data[field], rules);
if (error) {
errors[field] = error;
}
}
}
return errors;
};
// Schema específico para todos
export const todoValidationSchema: ValidationSchema<TodoFormData> = {
title: {
required: true,
minLength: 1,
maxLength: 100,
},
description: {
maxLength: 500,
},
priority: {
required: true,
custom: (value) => {
const validPriorities = ['low', 'medium', 'high', 'urgent'];
return validPriorities.includes(value) ? undefined : 'Prioridad inválida';
},
},
categoryId: {
required: true,
},
dueDate: {
custom: (value) => {
if (value && new Date(value) < new Date()) {
return 'La fecha no puede ser en el pasado';
}
return undefined;
},
},
};
export const validateTodoForm = (data: TodoFormData): TodoFormErrors => {
return validateForm(data, todoValidationSchema);
};
🧪 Ejemplo de Uso Completo
Screen con Tipado Completo
Crea src/screens/TodoFormScreen.tsx:
// src/screens/TodoFormScreen.tsx
import React, { useState, useEffect } from 'react';
import { View, StyleSheet, ScrollView, Alert } from 'react-native';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { useTypedNavigation } from '@/hooks/useTypedNavigation';
import { validateTodoForm } from '@/utils/validators';
import { TodoFormData, TodoFormErrors, Priority } from '@/types/todo';
import { RootStackScreenProps } from '@/types/navigation';
type Props = RootStackScreenProps<'TodoForm'>;
export const TodoFormScreen: React.FC<Props> = ({ route }) => {
const navigation = useTypedNavigation();
const { todoId, mode } = route.params;
const [formData, setFormData] = useState<TodoFormData>({
title: '',
description: '',
priority: 'medium' as Priority,
categoryId: '',
dueDate: '',
tags: '',
});
const [errors, setErrors] = useState<TodoFormErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const updateField = <K extends keyof TodoFormData>(
field: K,
value: TodoFormData[K]
) => {
setFormData(prev => ({ ...prev, [field]: value }));
// Limpiar error del campo cuando el usuario empiece a escribir
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: undefined }));
}
};
const handleSubmit = async () => {
const validationErrors = validateTodoForm(formData);
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
setIsSubmitting(true);
try {
// Aquí iría la lógica de guardado
// await saveTodo(formData);
Alert.alert(
'Éxito',
mode === 'create' ? 'Tarea creada' : 'Tarea actualizada',
[{ text: 'OK', onPress: () => navigation.goBack() }]
);
} catch (error) {
Alert.alert('Error', 'No se pudo guardar la tarea');
} finally {
setIsSubmitting(false);
}
};
return (
<ScrollView style={styles.container}>
<View style={styles.form}>
<Input
label="Título"
placeholder="Ingresa el título de la tarea"
value={formData.title}
onChangeText={(text) => updateField('title', text)}
error={errors.title}
maxLength={100}
/>
<Input
label="Descripción"
placeholder="Descripción opcional"
value={formData.description}
onChangeText={(text) => updateField('description', text)}
error={errors.description}
multiline
numberOfLines={3}
maxLength={500}
/>
<Button
title={mode === 'create' ? 'Crear Tarea' : 'Actualizar Tarea'}
onPress={handleSubmit}
loading={isSubmitting}
style={styles.submitButton}
/>
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
form: {
padding: 16,
},
submitButton: {
marginTop: 24,
},
});
✅ Resumen del Capítulo
En este capítulo hemos aprendido a:
- Configurar TypeScript avanzado con tipos globales y path mapping
- Crear tipos robustos para toda la aplicación (Todo, User, API)
- Tipar componentes correctamente con interfaces detalladas
- Implementar navegación tipada con React Navigation
- Usar tipos utilitarios para casos comunes
- Crear validadores tipados para formularios
- Integrar todo en componentes reales con tipado completo
🎯 Siguiente Capítulo
En el Capítulo 3: Primeros Componentes y Estilos, aprenderemos:
- Sistema de estilos profesional
- Componentes de UI reutilizables
- Flexbox y diseño responsive
- Temas y variables de diseño
¡Continúa para crear componentes hermosos y funcionales! 🎨