← Volver al listado de tecnologías

Capítulo 2: TypeScript en React Native

Por: Artiko
react-nativetypescripttiposinterfacescomponentes

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:

📋 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:

  1. Configurar TypeScript avanzado con tipos globales y path mapping
  2. Crear tipos robustos para toda la aplicación (Todo, User, API)
  3. Tipar componentes correctamente con interfaces detalladas
  4. Implementar navegación tipada con React Navigation
  5. Usar tipos utilitarios para casos comunes
  6. Crear validadores tipados para formularios
  7. Integrar todo en componentes reales con tipado completo

🎯 Siguiente Capítulo

En el Capítulo 3: Primeros Componentes y Estilos, aprenderemos:

¡Continúa para crear componentes hermosos y funcionales! 🎨