{"id":20709484,"url":"https://github.com/david-gmz/taskappts","last_synced_at":"2025-12-31T00:42:55.349Z","repository":{"id":263167223,"uuid":"889189005","full_name":"david-gmz/TaskAppTS","owner":"david-gmz","description":"Project task made on ReactJS 18 with TS","archived":false,"fork":false,"pushed_at":"2024-11-30T12:47:57.000Z","size":554,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-11T05:47:05.417Z","etag":null,"topics":["context-api","learning-by-doing","reactjs","reducer","tailwind-css","typescript","usecallback-hook","usememo-hook"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/david-gmz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-11-15T19:33:15.000Z","updated_at":"2024-11-29T21:06:12.000Z","dependencies_parsed_at":"2024-11-16T17:31:18.007Z","dependency_job_id":"ecfce1fe-5ad9-444f-badf-a03fe7c1e151","html_url":"https://github.com/david-gmz/TaskAppTS","commit_stats":null,"previous_names":["david-gmz/taskappts"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/david-gmz%2FTaskAppTS","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/david-gmz%2FTaskAppTS/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/david-gmz%2FTaskAppTS/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/david-gmz%2FTaskAppTS/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/david-gmz","download_url":"https://codeload.github.com/david-gmz/TaskAppTS/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":242980786,"owners_count":20216285,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["context-api","learning-by-doing","reactjs","reducer","tailwind-css","typescript","usecallback-hook","usememo-hook"],"created_at":"2024-11-17T02:06:35.786Z","updated_at":"2025-12-24T06:26:41.308Z","avatar_url":"https://github.com/david-gmz.png","language":"TypeScript","readme":"# React TS Task Project \u003cbr\u003e (TypeScript Journey Part II)\n\n## Intro\nThe main purpose of this project based on one of the lessons in the course \"React - The Complete Guide\"[^1] *by Maximilian Schwarzmuller* is to practice TypeScript. In addition *Context API*, *useReducer* and *Redux*, three popular ways of state managment.\n\n\u003cdetails\u003e\u003csummary\u003eDefine `{...props}` type\u003c/summary\u003e\n \n## How to define the spread property in a component\n### Narrowing came to the rescue\n \n I spent quite a lot of time trying to solve this doubt on how to type the spread property of a component what is shown in the next snippet:\n\n---\n\n ```ts\ninterface InputProps { \nisTextarea: boolean, \nlabel: string, \nprops: // Which type is this? \n} \n\nexport default function input({ isTextarea, label, ...props }: InputProps)\n{ \n  return ( \n    \u003cp\u003e\n      \u003clabel htmlFor=\"\"\u003e{label}\u003c/label\u003e \n      {isTextarea ? \u003ctextarea {...props} /\u003e : \u003cinput {...props} /\u003e} \n    \u003c/p\u003e \n) \n}\n ```\nFor the props field in the InputProps interface, we want to account for the different props accepted by `\u003ctextarea\u003e` and `\u003cinput\u003e`. Since textarea and input elements share many props but also have unique ones, we can use TypeScript's built-in utility types. \n\n## Solution\n\nWe can use a discriminated union to conditionally handle the props depending on the isTextarea flag. Here's how:\n\n```ts\nimport React from \"react\";\n\ninterface InputPropsBase {\n  label: string;\n}\n\ninterface InputPropsTextArea extends InputPropsBase {\n  isTextarea: true;\n  props?: React.TextareaHTMLAttributes\u003cHTMLTextAreaElement\u003e;\n}\n\ninterface InputPropsInput extends InputPropsBase {\n  isTextarea: false;\n  props?: React.InputHTMLAttributes\u003cHTMLInputElement\u003e;\n}\n\ntype InputProps = InputPropsTextArea | InputPropsInput;\n```\n## Explanation\n1. **Base Properties:**\u003cbr\u003e\n   * The label property is common to both cases, so it's extracted into a base interface InputPropsBase.\n\n2. **Conditional Props:**\u003cbr\u003e\n   * *InputPropsTextArea:* Includes isTextarea: true and allows `React.TextareaHTMLAttributes\u003cHTMLTextAreaElement\u003e as props`.\n   * *InputPropsInput:* Includes isTextarea: false and allows `React.InputHTMLAttributes\u003cHTMLInputElement\u003e as props`.\n\n3. **Discriminated Union:**\u003cbr\u003e\n   * Using `isTextarea` as the discriminator ensures that TypeScript will enforce the correct props type based on its value.\n\n4. **Default Props:**\u003cbr\u003e\n   * Added `props = {}` to avoid undefined props when spreading.\n\nHowever the spread operator `(...props)` does not automatically narrow the type of props to either `React.TextareaHTMLAttributes\u003cHTMLTextAreaElement\u003e` or `React.InputHTMLAttributes\u003cHTMLInputElement\u003e` based on `isTextarea`. Like:\n\n```ts\nexport default function Input({ isTextarea, label, props = {} }: InputProps) {\n  return (\n    \u003cp\u003e\n      \u003clabel htmlFor=\"\"\u003e{label}\u003c/label\u003e\n      {isTextarea ? (\n        \u003ctextarea {...props} /\u003e //This going to cause a mismatch\n      ) : (\n        \u003cinput {...props} /\u003e //This going to cause a mismatch\n      )}\n    \u003c/p\u003e\n  );\n}\n\n```\nIt's going to attempt to assign the full union of both types to each element, causing a mismatch for event handlers like `onChange`.\n\nWe need to narrow the type explicitly before spreading props. \n\n### Narrow Props Based on isTextarea\n\n```ts\nexport default function Input({ isTextarea, label, props = {} }: InputProps) {\n  if (isTextarea) {\n    // Narrow to TextArea props\n    const textareaProps = props as React.TextareaHTMLAttributes\u003cHTMLTextAreaElement\u003e;\n    return (\n      \u003cp\u003e\n        \u003clabel htmlFor=\"\"\u003e{label}\u003c/label\u003e\n        \u003ctextarea {...textareaProps} /\u003e\n      \u003c/p\u003e\n    );\n  } else {\n    // Narrow to Input props\n    const inputProps = props as React.InputHTMLAttributes\u003cHTMLInputElement\u003e;\n    return (\n      \u003cp\u003e\n        \u003clabel htmlFor=\"\"\u003e{label}\u003c/label\u003e\n        \u003cinput {...inputProps} /\u003e\n      \u003c/p\u003e\n    );\n  }\n}\n```\n## Explanation \n1. **Explicit Type Narrowing:**\u003cbr\u003e\n  Before spreading props, explicitly cast props to the correct type (`TextareaHTMLAttributes` or `InputHTMLAttributes`) using a `const` assignment.This ensures TypeScript knows the exact type of props when spreading into the respective element.\n\n2. **Union Resolution:**\u003cbr\u003e\n  The conditional `if (isTextarea)` ensures TypeScript understands which branch is active, allowing us to safely narrow props.\n\n3. **Safe Spreading:**\u003cbr\u003e\n  After narrowing, spreading `textareaProps` or `inputProps` will no longer throw type errors, as their types align perfectly with the attributes of `\u003ctextarea\u003e` and `\u003cinput\u003e` respectively.\n\n***TypeScript's type narrowing** requires clear distinctions in code flow, and unions don’t automatically propagate to props when destructuring. By explicitly casting and separating the logic, we ensure correctness.*\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eMore on spread props\u003c/summary\u003e\n \n##  Using `onClick` in a `\u003cbutton\u003e`\n```ts\nimport React from \"react\";\n\ninterface ButtonProps extends React.ButtonHTMLAttributes\u003cHTMLButtonElement\u003e {\n  label: string;\n}\n\nexport default function Button({ label, ...props }: ButtonProps) {\n  return (\n    \u003cbutton {...props}\u003e\n      {label}\n    \u003c/button\u003e\n  );\n}\n```\n###  Explanation:\nBy extending `React.ButtonHTMLAttributes\u003cHTMLButtonElement\u003e`, the Button component automatically supports all valid attributes of a `\u003cbutton\u003e`, such as onClick, disabled, type, etc.\n \n### TypeScript Validation\n1. **TypeScript ensures that:**\n\n   - `onClick` is properly typed as \u003cbr\u003e\n   `(event: React.MouseEvent\u003cHTMLButtonElement\u003e) =\u003e void.`\n   - Other invalid attributes are caught. For example, passing an invalid attribute like rows to a `\u003cbutton\u003e` would result in an error:\n```ts\n\u003cButton label=\"Invalid Button\" rows={3} /\u003e // ❌ Error: 'rows' does not exist on type 'ButtonHTMLAttributes\u003cHTMLButtonElement\u003e'\n```\n### Key Takeaways\n   - onClick is an intrinsic attribute of `\u003cbutton\u003e`, and we don’t need to define it explicitly in our interface when extending `React.ButtonHTMLAttributes\u003cHTMLButtonElement\u003e`.\n   - Using TypeScript’s intrinsic attributes for HTML elements ensures our props are aligned with the standard DOM attributes.\n\n### Why Use label Instead of children?\n1. **Semantic Clarity:**\n   - label explicitly communicates that the string is the button's text content.\n   - children is more generic and implies flexibility (e.g., the ability to nest other components).\n2. **Consistency:**\n   - If your component has other structured props (like icon, variant, etc.), using label keeps the API clear and avoids ambiguity:\n```ts\n\u003cButton label=\"Click Me\" icon={\u003cIcon /\u003e} variant=\"primary\" /\u003e;\n```\n3. **Flexibility for Other Features:**\n   - If we later decide to allow additional customizations (like an optional icon or aria-label for accessibility), having a dedicated label makes it easier to manage:\n```ts\ninterface ButtonProps extends React.ButtonHTMLAttributes\u003cHTMLButtonElement\u003e {\n  label: string; // Text shown on the button\n  icon?: React.ReactNode; // Optional icon to display\n}\n\n\u003cButton label=\"Click Me\" icon={\u003cIcon /\u003e} /\u003e;\n```\n### Comparison\nUsing children:\n```ts\n\u003cButton onClick={() =\u003e alert(\"Clicked!\")}\u003eClick Me\u003c/Button\u003e;\n```\nUsing label:\n```ts\n\u003cButton onClick={() =\u003e alert(\"Clicked!\")} label=\"Click Me\" /\u003e;\n```\nBoth work, but the second option (label) is more explicit for text-only buttons.\n\u003c/details\u003e\n\u003cdetails\u003e\u003csummary\u003eTyping forwardRef in React\u003c/summary\u003e\n \n## Properly type our React.forwardRef function for Input\n### handle the ref argument\nSo starting with this part of the Input component:\n```ts\nconst Input = React.forwardRef(function Input({ isTextarea, label, props = {} }: InputProps, ref) {\n    //code...\n    \u003ctextarea ref={ref} className={classesInput} {...textareaProps} /\u003e\n    //more code...\n    \u003cinput ref={ref} className={classesInput} {...inputProps} /\u003e\n    })\n```\n To properly type our `React.forwardRef` function for `Input`, we need to handle the ref argument and its typing. Since ref will either point to a textarea or an input element based on the `isTextarea` prop, you'll need to define a generic type that accommodates both.\n\nHere’s the updated and typed `React.forwardRef` implementation:\n```ts\n// models.read-the-docs\ninterface InputPropsBase {\n    label: string;\n}\n\ninterface InputPropsTextArea extends InputPropsBase {\n    isTextarea: true;\n    props?: React.TextareaHTMLAttributes\u003cHTMLTextAreaElement\u003e;\n}\n\ninterface InputPropsInput extends InputPropsBase {\n    isTextarea: false;\n    props?: React.InputHTMLAttributes\u003cHTMLInputElement\u003e;\n}\n\ntype InputProps = InputPropsTextArea | InputPropsInput;\n\n// Input.tsx\nconst Input = React.forwardRef\u003c\n    HTMLTextAreaElement | HTMLInputElement,\n    InputProps\n\u003e(function Input({ isTextarea, label, props = {} }: InputProps, ref) {\n  //code...\n    \u003ctextarea\n        ref={ref as React.Ref\u003cHTMLTextAreaElement\u003e}\n        className={classesInput}\n        {...textareaProps}\n    /\u003e\n    //more code...\n    \u003cinput\n        ref={ref as React.Ref\u003cHTMLInputElement\u003e}\n        className={classesInput}\n        {...inputProps}\n    /\u003e\n    })\n```\n### Key Changes and Explanation:\n1. **ForwardRef Type:**\n   - The `React.forwardRef` generic type is defined as `\u003cHTMLTextAreaElement | HTMLInputElement, InputProps\u003e`.\n   - This ensures the ref can point to either an HTMLTextAreaElement or an `HTMLInputElement`, based on `isTextarea`.\n2. **Casting ref:**\n   - Inside the conditional branches, the ref is cast to the appropriate type using `React.Ref\u003cHTMLTextAreaElement\u003e` or `React.Ref\u003cHTMLInputElement\u003e`.\n3. **Fallback for props:**\n   - The props property in InputProps is still optional and defaults to an empty object ({}).\n### Usage:\nWhen consuming the Input component with a ref, TypeScript will correctly infer the type based on the isTextarea prop:\n```ts\n// NewProject.tsx\nimport React from \"react\";\nimport Input from \"./Input\";\n\nexport default function NewProject() {\n\n    constefining Dtype  = in Modal component  React.useRef\u003cHTMLInputElement\u003e(null);\n    const description = React.useRef\u003cHTMLTextAreaElement\u003e(null);\n    const dueDate = React.useRef\u003cHTMLInputElement\u003e(null);\n     return (\n      // code component\n       \u003cInput\n                    ref={title}\n                    isTextarea={false}\n                    label=\"Title\"\n                    props={{ type: \"text\", placeholder: \"Enter text\" }}\n                /\u003e\n                \u003cInput\n                    ref={description}\n                    label=\"Description\"\n                    props={{ placeholder: \"Enter your description\" }}\n                    isTextarea\n                /\u003e\n                \u003cInput\n                    ref={dueDate}\n                    isTextarea={false}\n                    label=\"Due Date\"\n                    props={{ type: \"date\" }}\n                /\u003e\n      //more code...\n     )\n   }\n ```\n\u003c/details\u003e\n\u003cdetails\u003e\u003csummary\u003especific type of the dialog ref \u003c/summary\u003e\n \n## Defining type in Modal component \n```ts\n//Modal.tsx\n//code of the component\n            open() {\n                if (dialog.current !== undefined) dialog.current.showModal(); // ❌Error: Property 'showModal' does not exist on type 'never'.ts(2339) \n            }\n//some more code..\n            \u003cdialog ref={dialog}\u003e{children}\u003c/dialog\u003e, // ❌Error:  Type 'MutableRefObject\u003cundefined\u003e' is not assignable to type 'LegacyRef\u003cHTMLDialogElement\u003e | undefined'. Type 'MutableRefObject\u003cundefined\u003e' is not assignable to type 'RefObject\u003cHTMLDialogElement\u003e'. Types of property 'current' are incompatible. Type 'undefined' is not assignable to type 'HTMLDialogElement | null'.ts(2322)\n```\n###  Solution\n By default, `React.useRef()` is initialized with undefined, leading TypeScript to infer the type as `MutableRefObject\u003cundefined\u003e`. Since we're working with an HTML `\u003cdialog\u003e` element, we should provide the correct type for the dialog `ref: HTMLDialogElement | null`.\nHere's a corrected version of your Modal component:\n\n```ts\n// Ensure the modal root exists\nconst modalRoot = document.getElementById(\"modal-root\");\n\nconst Modal = forwardRef(function Modal(\n  { children }: ModalProps,\n  ref: React.Ref\u003c{ open: () =\u003e void }\u003e\n) {\n  // Define the ref with the correct type\n  const dialog = useRef\u003cHTMLDialogElement | null\u003e(null);\n\n  // Use imperative handle to expose functions to the parent component\n  useImperativeHandle(ref, () =\u003e ({\n    open() {\n      if (dialog.current) {\n        dialog.current.showModal();\n      }\n    },\n  }));\n\n  // Render the dialog inside the modal root using portals\n  if (modalRoot) {\n    return createPortal(\n      \u003cdialog ref={dialog}\u003e{children}\u003c/dialog\u003e,\n      modalRoot\n    );\n  }\n\n  return null;\n});\n```\n## Explanation of Fixes:\n1. **Type for dialog Ref:**\n   - Changed `const dialog = useRef();` to `const dialog = useRef\u003cHTMLDialogElement | null\u003e(null);` to specify that the dialog ref references an HTML `\u003cdialog\u003e` element.\n2. **useImperativeHandle Type:**\n   - Defined the type of `ref` as `React.Ref\u003c{ open: () =\u003e void }\u003e` to specify that the parent component can use the open function.\n3. **Portal Check:**\n   - Added a check for modalRoot to handle cases where modal-root is missing, ensuring a graceful fallback.\n4. **createPortal Typing:**\n   - Fixed dialog ref type mismatch by ensuring it matches the expected type `React.RefObject\u003cHTMLDialogElement\u003e`.\n*This should eliminate the TypeScript errors and ensure proper type safety in your component.*\n\n## Why ref as React.Ref\u003c{ open: () =\u003e void }\u003e?\n1. **Default Behavior of ref:**\n   - Normally, a ref in React points directly to an element (`HTMLDialogElement`, `HTMLDivElement`, etc.). However, when you use `forwardRef` with imperative handles, you're essentially customizing what the parent can \"see\" through that ref.\n\n2. **Custom API for the Parent:**\n   - Instead of exposing the raw DOM node (HTMLDialogElement), you're exposing an object with specific methods, like `{ open: () =\u003e void }`. TypeScript requires we to explicitly define the shape of that object.\n\n3. **React's Ref Type:**\u003cbr\u003e\nThe type `React.Ref\u003cT\u003e` represents a ref that can either:\n   - Be a callback ref (function).\n   - Be a `RefObject\u003cT\u003e` (created by useRef).\n   - Or be null.\n\nSince our component will expose the open() method, we declare the ref type as `React.Ref\u003c{ open: () =\u003e void }\u003e`, letting TypeScript know exactly what the parent will receive.\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003efix the typing issue in our handleAddTask function.\u003c/summary\u003e\n \n## Match the TaskProps\n```ts\n// App.tsx\nconst handleAddTask = (text: string) =\u003e {\n    setStateProjects((prevStateProjects: InitState) =\u003e {\n        if (!prevStateProjects.selectedProjectId) {\n            return prevStateProjects; // Return unchanged if no project is selected\n        }\n        \n        const newTask: TaskProps = {\n            id: prevStateProjects.selectedProjectId,\n            text,\n            taskId: Date.now()\n        };\n        return {\n            ...prevStateProjects,\n            tasks: [newTask, ...prevStateProjects.tasks]\n        };\n    });\n};\n```\nThat was a first solution. Then I added ainterface TasksProps {tasks: Task[]}\n\u003c/details\u003e\n\n\u003cdetails open\u003e\u003csummary\u003eRefactoring with Context API redefining types\u003c/summary\u003e\n \n##  Advantages of Context API with TypeScript\n1. **Avoids Prop Drilling**\n\n   - Context simplifies the process of passing data deeply nested in a component tree. This is especially useful for global states like themes, authentication, or language preferences.\n2. **Improved Type Safety**\n\n   - TypeScript ensures that the context's shape is consistent across components. With well-defined types, developers are less prone to runtime errors caused by mismatched data structures.\n3. **Better Developer Experience**\n\n   - Intellisense in IDEs (e.g., VSCode) leverages TypeScript types, making it easier to use context values correctly and reducing the learning curve for new developers.\n4. **Scalability for Small to Medium Apps**\n\n   - Context API works well for apps with manageable state requirements, providing a simpler alternative to libraries like Redux for medium-sized projects.\n\n\u003c/details\u003e\n---\n\n[^1]: This course can be found in Udemy website.","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavid-gmz%2Ftaskappts","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdavid-gmz%2Ftaskappts","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavid-gmz%2Ftaskappts/lists"}