Skip to Content
Made with 🖤 by @1chooo
Wiki

Wiki

Here, we will discuss the problems and solutions that we have encountered in the development process.

Support for TypeScript 5.x in Create React App

In the https://github.com/1chooo/1chooo.com/pull/76, when I tried to upgrade the react-scripts to 5.x, I got the following error:

npm error code ERESOLVE npm error ERESOLVE could not resolve npm error npm error While resolving: react-scripts@5.0.1 npm error Found: typescript@5.5.4 npm error node_modules/typescript npm error typescript@"^5.5.4" from the root project npm error peer typescript@">= 2.7" from fork-ts-checker-webpack-plugin@6.5.3 npm error node_modules/fork-ts-checker-webpack-plugin npm error fork-ts-checker-webpack-plugin@"^6.5.0" from react-dev-utils@12.0.1 npm error node_modules/react-dev-utils npm error react-dev-utils@"^12.0.1" from react-scripts@5.0.1 npm error node_modules/react-scripts npm error react-scripts@"5.0.1" from the root project npm error 1 more (tsutils) npm error npm error Could not resolve dependency: npm error peerOptional typescript@"^3.2.1 || ^4" from react-scripts@5.0.1 npm error node_modules/react-scripts npm error react-scripts@"5.0.1" from the root project

Add the following to the package.json to solve the problem: 1

+ "overrides": { + "typescript": "^5.5.4" + },

[!IMPORTANT] The version of overrides must be the same as the version of TypeScript in dependencies.

Migrate from Create React App to NextJS

"scripts": { "dev": "next dev", "build": "next build", "start": "next start" },
$ npm uninstall react-scripts $ npm uninstall react-router-dom $ npm install next

Dangerously Set innerHTML

https://dev.to/shareef/rendering-markdown-made-easy-with-react-markdown-in-reactjs-and-nextjs-web-apps-259d

In HTML, <div> cannot be a descendant of <p>. This will cause a hydration error.
... <Markdown> <p> ^^^ <http://localhost:3000/_next/static/chunks/src_dd03ef._.js:150:225> <div>
+ const isImageNode = (node: any): node is Element => { + return node && node.type === 'element' && node.tagName === 'img'; + }; const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content }) => ( <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} components={{ + p: ({ node, children }) => { + const hasImage = node && node.children && node.children.some(isImageNode); + if (hasImage) { + return <>{children}</>; + } + return <p>{children}</p>; + }, ... > {content} </ReactMarkdown> ); export default MarkdownRenderer;

React Wrap Balancer

React Wrap Balancer is a simple React Component that makes your titles more readable in different viewport sizes. It improves the wrapping to avoid situations like single word in the last line, makes the content more “balanced”: 1

Simple React Component That Makes Titles More Readable

Usage

We have to install the package first:

npm i react-wrap-balancer

Then we can use it in our project:

import Balancer from 'react-wrap-balancer' // ... function Title() { return ( <h1> <Balancer>My Awesome Title</Balancer> </h1> ) }

If we have multiple <Balancer> components used, we can wrap them with <Provider> to share the re-balance logic:

import { Provider } from 'react-wrap-balancer' // ... function App() { return ( <Provider> <MyApp/> </Provider> ) }

Next.js Image Fast Loading

SEO Tools

Open Graph Meta Tags

Loose Autocomplete

acceptString = (str: "foo" | "bar" | (string & {})) => { console.log(str); }; acceptString("baz");

https://www.threads.net/@jimmy.chiang/post/C_vl632ygU_/?xmt=AQGzwdnxgbmCCfAF8xCgI2zZiemQtJDR7omD6Mb26Ge3CA

https://www.youtube.com/live/8HoOxOd86M4?t=2778&si=P2mxsBXVq8UmrAX_

Robots.txt

/** * https://www.cloudflare.com/zh-tw/learning/bots/what-is-robots-txt/ * https://www.cloudflare.com/robots.txt * https://github.com/vercel/vercel/blob/3e4223684609dbdb7d9a2b286294fe07941bf0d4/examples/hydrogen-2/app/routes/%5Brobots.txt%5D.tsx# * https://github.com/vercel/vercel/blob/3e4223684609dbdb7d9a2b286294fe07941bf0d4/packages/cli/test/dev/integration-2.test.ts# * https://github.com/vercel/vercel/blob/3e4223684609dbdb7d9a2b286294fe07941bf0d4/examples/hydrogen/src/routes/robots.txt.server.ts * @returns */ const robotsTxtContent = ` # ________ # __,_, | | # [_|_/ | OK | # // |________| # _// __ / # (_|) |@@| # \\ \\__ \\--/ __ # \\o__|----| | __ # \\ }{ /\\ )_ / _\\ # /\\__\\/ \\__O (__ # (--/\\--) \\__/ # _)( )(_ # \`---''---\` User-agent: * Disallow: `;
/** * This API endpoint generates a robots.txt file. Use this to control * access to your resources from SEO crawlers. * Learn more: https://developers.google.com/search/docs/advanced/robots/create-robots-txt */ import type {HydrogenRequest} from '@shopify/hydrogen'; export async function api(request) { const url = new URL(request.url); return new Response(robotsTxtData({url: url.origin}), { headers: { 'content-type': 'text/plain', // Cache for 24 hours 'cache-control': `max-age=${60 * 60 * 24}`, }, }); } function robotsTxtData({url}: {url: string}) { const sitemapUrl = url ? `${url}/sitemap.xml` : undefined; return ` User-agent: * Disallow: /admin Disallow: /cart Disallow: /orders Disallow: /checkouts/ Disallow: /checkout Disallow: /carts Disallow: /account ${sitemapUrl ? `Sitemap: ${sitemapUrl}` : ''} # Google adsbot ignores robots.txt unless specifically named! User-agent: adsbot-google Disallow: /checkouts/ Disallow: /checkout Disallow: /carts Disallow: /orders User-agent: Pinterest Crawl-delay: 1 `.trim(); }

Font Optimization

refactor(font): add Roboto font and update layout to optimize with next/font (#556)

Before

src/app/global.css
.root { /* font-family */ - --ff-poppins: "Poppins", sans-serif; } - html { - font-family: var(--ff-poppins); - }
src/app/font.ts
import { Roboto } from "next/font/google"; export const roboto = Roboto({ weight: ['400', '700'], style: ['normal', 'italic'], subsets: ['latin'], display: 'swap', })
src/app/layout.tsx
import { roboto } from "./font"; const RootLayout = (props: RootLayoutProps) => { const { children } = props return ( - <html lang="en"> + <html lang="en" className={`${roboto.className}`}>

Dynamic Manifest

Next.js provides a convenient way to generate a web app manifest dynamically using its metadata API. This approach allows you to customize your manifest based on your application’s configuration or environment. This allows browsers to present the web app similarly to native applications, enabling features like installation on the home screen and full-screen display. 2

RSS Feed

Minimize main-thread work

How to minimize the “Minimize main-thread work” in nextjs? #19436

How to minimize main thread work in React Component https://aatifbandey.medium.com/reduce-main-thread-work-in-react-component-a90c9bc1d9b3

Web Vitals

https://github.com/luciancah/nextjs-ko/blob/7f67677d32f7247d0d468f1b3e1bceb43a6e03bd/pages/docs/app/building-your-application/optimizing/analytics.mdx#L17 https://github.com/axiomhq/next-axiom/tree/0ad6cf706dc154b17bd65e11d850a8c2b710db61/src/webVitals

Using setTimeout with Promise for Delayed Execution

When testing rendering effects or simulating delays in asynchronous operations, you can use setTimeout wrapped in a Promise. This approach ensures clean and readable code, especially in async/await contexts.

Code Example

To add a timeout in your tests or development workflows:

await new Promise(resolve => setTimeout(resolve, 3000));

Explanation

  1. setTimeout: Executes a function after a specified delay (in milliseconds).
  2. Promise: Allows setTimeout to work seamlessly with async/await.
  3. 3000: Represents the delay in milliseconds (3 seconds in this example).

When to Use

  • To simulate network latency or slow computations.
  • To test skeleton loaders or other rendering effects.
  • To delay execution for debugging or animations.

Usage in a Function

async function simulateLoadingEffect() { console.log('Loading...'); await new Promise(resolve => setTimeout(resolve, 3000)); console.log('Done!'); } simulateLoadingEffect();

Output

  1. Displays “Loading…”
  2. Waits for 3 seconds
  3. Displays “Done!“

application/ld+json

Example - Apple

<script type="application/ld+json"> { "@context": "http://schema.org", "@id": "https://www.apple.com/#organization", "@type": "Organization", "name": "Apple", "url": "https://www.apple.com/", "logo": "https://www.apple.com/ac/structured-data/images/knowledge_graph_logo.png?202110180743", "subOrganization": { "@type": "Organization", "name": "Apple Support", "url": "https://support.apple.com", "@id": "https://support.apple.com/#organization" }, "contactPoint": [ { "@type": "ContactPoint", "telephone": "+1-800-692-7753", "contactType": "sales", "areaServed": "US" }, { "@type": "ContactPoint", "telephone": "+1-800-275-2273", "contactType": "technical support", "areaServed": "US", "availableLanguage": ["EN", "ES"] }, { "@type": "ContactPoint", "telephone": "+1-800-275-2273", "contactType": "customer support", "areaServed": "US", "availableLanguage": ["EN", "ES"] } ], "sameAs": [ "http://www.wikidata.org/entity/Q312", "https://www.youtube.com/user/Apple", "https://www.linkedin.com/company/apple", "https://www.facebook.com/Apple", "https://www.twitter.com/Apple" ] } </script>

Example - honghong.me

https://github.com/tszhong0411/honghong.me/blob/main/apps/web/src/app/page.tsx

$ npm i schema-dts
const jsonLd: WithContext<WebSite> = { '@context': 'https://schema.org', '@type': 'WebSite', name: SITE_TITLE, description: SITE_DESCRIPTION, url: SITE_URL, author: { '@type': 'Person', name: SITE_NAME, url: SITE_URL, sameAs: [SITE_FACEBOOK_URL, SITE_INSTAGRAM_URL, SITE_X_URL, SITE_GITHUB_URL, SITE_YOUTUBE_URL] }, mainEntityOfPage: { '@type': 'WebPage', '@id': SITE_URL }, inLanguage: 'en-US', copyrightYear: new Date().getFullYear(), keywords: SITE_KEYWORDS, dateCreated: '2020-12-05', dateModified: new Date().toISOString() } <script type='application/ld+json' dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />

References

Debugging Tools

https://developers.facebook.com/tools/debug/

png to webp

https://cloudconvert.com/png-to-webp

⨯ ESLint: Failed to load plugin 'jest' declared in '.eslintrc.js': Cannot find module 'eslint-plugin-jest' Require stack: - /Users/hugolin/Developer/1chooo.com/apps/web/__placeholder__.js ✓ Linting and checking validity of types

https://stackoverflow.com/questions/71781215/typeerror-failed-to-load-plugin-jest-declared-in-eslintrc-class-extends-v

⚠ Found lockfile missing swc dependencies, patching... ⚠ Lockfile was successfully patched, please run "npm install" to ensure @next/swc dependencies are downloaded
⨯ You are using configuration and/or tools that are not yet supported by Next.js with Turbopack: - Unsupported Next.js configuration option(s) (next.config.js) To use Turbopack, remove the following configuration options: - experimental.forceSwcTransforms If you cannot make the changes above, but still want to try out Next.js with Turbopack, create the Next.js playground app by running the following commands: npx create-next-app --example with-turbopack with-turbopack-app cd with-turbopack-app npm run dev ⚠ Learn more about Next.js and Turbopack: https://nextjs.link/with-turbopack
⚠ Using edge runtime on a page currently disables static generation for that page

https://nextjs.org/docs/messages/swc-disabled

https://buildui.com/posts/global-progress-in-nextjs

https://convertio.co/

export type Resume = { icon: VCardIconType; title: string; timeLines: TimeLine[]; sectionType: string; }
export type Config = { resumes: Resume[]; };
resumes: [ { icon: IoSchoolOutline, title: "Education", sectionType: "education", timeLines: [ { company: "University of Southern California", companyImage: "/images/logos/usc.jpg", title: "Master of Science in Computer Science", employmentType: "Viterbi School of Engineering", location: "Los Angeles, CA 🇺🇸", timePeriod: "Aug. 2025 - Present", }, { company: "National Central University", companyImage: "/images/logos/ncu.png", title: "Bachelors of Science in Atmospheric Science", employmentType: "College of Earth Sciences", location: "Taoyuan, Taiwan 🇹🇼", timePeriod: "Sep. 2020 - Jun. 2024", } ] }, { icon: Icons.Devices, title: "Professional Experiences", sectionType: "experience", timeLines: [ { company: "eCloudvalley Digital Technology", companyImage: "/images/logos/ecv.png", title: "Cloud Engineer", employmentType: "Intern", location: "New Taipei, Taiwan 🇹🇼", timePeriod: "Mar. 2024 - May. 2024", }, { company: "Amazon Web Services", companyImage: "/images/logos/aws.svg", title: "Campus Ambassador", employmentType: "Contract", location: "Taipei, Taiwan 🇹🇼", timePeriod: "Aug. 2023 - Feb. 2024", }, { company: "PEGATRON", companyImage: "/images/logos/pegatron.png", title: "AI Engineer", employmentType: "Intern", location: "Taipei, Taiwan 🇹🇼", timePeriod: "Jul. 2023 - Aug. 2023", }, { company: "National Central University", companyImage: "/images/logos/ncu.png", title: "Website Developer", employmentType: "Part-time", location: "Taoyuan, Taiwan 🇹🇼", timePeriod: "Jul. 2022 - Jan. 2023", }, ], } ],

目前是這樣,我可不可以把 sectionType extract 出來,讓我的結構變成:

resumes = [ educations: { icon: IoSchoolOutline, title: "Education", timeLines: [ { company: "University of Southern California", companyImage: "/images/logos/usc.jpg", title: "Master of Science in Computer Science", employmentType: "Viterbi School of Engineering", location: "Los Angeles, CA 🇺🇸", timePeriod: "Aug. 2025 - Present", }, { company: "National Central University", companyImage: "/images/logos/ncu.png", title: "Bachelors of Science in Atmospheric Science", employmentType: "College of Earth Sciences", location: "Taoyuan, Taiwan 🇹🇼", timePeriod: "Sep. 2020 - Jun. 2024", } ] }, experiences: { icon: Icons.Devices, title: "Professional Experiences", timeLines: [ { company: "eCloudvalley Digital Technology", companyImage: "/images/logos/ecv.png", title: "Cloud Engineer", employmentType: "Intern", location: "New Taipei, Taiwan 🇹🇼", timePeriod: "Mar. 2024 - May. 2024", }, { company: "Amazon Web Services", companyImage: "/images/logos/aws.svg", title: "Campus Ambassador", employmentType: "Contract", location: "Taipei, Taiwan 🇹🇼", timePeriod: "Aug. 2023 - Feb. 2024", }, { company: "PEGATRON", companyImage: "/images/logos/pegatron.png", title: "AI Engineer", employmentType: "Intern", location: "Taipei, Taiwan 🇹🇼", timePeriod: "Jul. 2023 - Aug. 2023", }, { company: "National Central University", companyImage: "/images/logos/ncu.png", title: "Website Developer", employmentType: "Part-time", location: "Taoyuan, Taiwan 🇹🇼", timePeriod: "Jul. 2022 - Jan. 2023", }, ], } ]

此外使用者就可以增加自己的欄位,像是 Teaching, Awards 等等的擴充

app/resume/page.tsx

import type { Metadata } from "next" import PageHeader from "@/components/page-header" import { Icons } from "@/components/icons" import ResumeSection from "@/components/section/resume" import config from "@/config" import { IoSchoolOutline } from "react-icons/io5" import type { ResumeData } from "@/types/resume" const { title } = config // New resume data structure const resumeData: ResumeData = { education: { icon: IoSchoolOutline, title: "Education", timeLines: [ { company: "University of Southern California", companyImage: "/images/logos/usc.jpg", title: "Master of Science in Computer Science", employmentType: "Viterbi School of Engineering", location: "Los Angeles, CA 🇺🇸", timePeriod: "Aug. 2025 - Present", }, { company: "National Central University", companyImage: "/images/logos/ncu.png", title: "Bachelors of Science in Atmospheric Science", employmentType: "College of Earth Sciences", location: "Taoyuan, Taiwan 🇹🇼", timePeriod: "Sep. 2020 - Jun. 2024", }, ], }, experience: { icon: Icons.Devices, title: "Professional Experiences", timeLines: [ { company: "eCloudvalley Digital Technology", companyImage: "/images/logos/ecv.png", title: "Cloud Engineer", employmentType: "Intern", location: "New Taipei, Taiwan 🇹🇼", timePeriod: "Mar. 2024 - May. 2024", }, { company: "Amazon Web Services", companyImage: "/images/logos/aws.svg", title: "Campus Ambassador", employmentType: "Contract", location: "Taipei, Taiwan 🇹🇼", timePeriod: "Aug. 2023 - Feb. 2024", }, { company: "PEGATRON", companyImage: "/images/logos/pegatron.png", title: "AI Engineer", employmentType: "Intern", location: "Taipei, Taiwan 🇹🇼", timePeriod: "Jul. 2023 - Aug. 2023", }, { company: "National Central University", companyImage: "/images/logos/ncu.png", title: "Website Developer", employmentType: "Part-time", location: "Taoyuan, Taiwan 🇹🇼", timePeriod: "Jul. 2022 - Jan. 2023", }, ], }, // Users can add more sections here like: // awards: { ... }, // teaching: { ... }, // projects: { ... } } export const metadata: Metadata = { title: `Resume | ${title}`, } export default function Resume() { return ( <article> <PageHeader header="Hugo's Resume" /> {Object.entries(resumeData).map(([sectionType, section]) => ( <ResumeSection key={sectionType} icon={section.icon} title={section.title} timeLines={section.timeLines} sectionType={sectionType} /> ))} </article> ) }

resume-card.tsx

"use client" import Image from "next/image" import { CalendarIcon, BriefcaseIcon, MapPin, School } from "lucide-react" import type { TimeLine } from "@/types/resume" import "@/styles/skills-bar.css" interface ResumeCardProps { timeLine: TimeLine sectionType: string } export default function ResumeCard({ timeLine, sectionType }: ResumeCardProps) { const { company, companyImage, title, employmentType, location, timePeriod } = timeLine return ( <section className="skill"> <div className="skills-list content-card"> <div className="flex flex-row items-center gap-4 p-6 pb-4"> <div className="h-12 w-12 overflow-hidden rounded-md"> <Image src={companyImage || "/favicon.ico"} alt={company} className="h-full w-full object-cover" onError={(e) => { e.currentTarget.src = "/favicon.ico?height=40&width=40" e.currentTarget.onerror = null }} width={40} height={40} /> </div> <div className="space-y-1"> <div className="font-semibold text-white-1">{company}</div> <div className="text-sm text-light-gray">{title}</div> </div> </div> <div className="px-6 pb-2"> <div className="mb-4 flex flex-wrap gap-2"> <span className="inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-medium text-orange-yellow-crayola border-gray-700"> {sectionType === "education" ? ( <School className="h-3 w-3" /> ) : sectionType === "experience" ? ( <BriefcaseIcon className="h-3 w-3" /> ) : null} {employmentType} </span> <span className="inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-medium text-orange-yellow-crayola border-gray-700"> <MapPin className="h-3 w-3" /> {location} </span> <span className="inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-medium text-orange-yellow-crayola border-gray-700"> <CalendarIcon className="h-3 w-3" /> {timePeriod} </span> </div> </div> </div> </section> ) }

section/resume.tsx

import IconBox from "@/components/icon-box" import ResumeCard from "@/components/resume/resume-card" import type { VCardIconType } from "@/types/config" import type { TimeLine } from "@/types/resume" import "@/styles/resume/timeline-item.css" interface ResumeSectionProps { icon: VCardIconType title: string timeLines: TimeLine[] sectionType: string } function ResumeSection({ icon, title, timeLines, sectionType }: ResumeSectionProps) { return ( <section className="timeline"> <div className="flex items-center gap-4 mb-6"> <IconBox icon={icon} /> <h3 className="text-white-2 text-2xl font-bold">{title}</h3> </div> <ol className="timeline-list"> {timeLines.map((timeLine: TimeLine) => ( <li className="timeline-item" key={timeLine.company}> <ResumeCard timeLine={timeLine} sectionType={sectionType} /> </li> ))} </ol> </section> ) } export default ResumeSection
import type { ResumeData } from "@/types/resume" import { IoSchoolOutline } from "react-icons/io5" import { Icons } from "@/components/icons" // Example of how you might include this in your config const config = { title: "Your Portfolio Title", // Other config properties... resumeData: { education: { icon: IoSchoolOutline, title: "Education", timeLines: [ { company: "University of Southern California", companyImage: "/images/logos/usc.jpg", title: "Master of Science in Computer Science", employmentType: "Viterbi School of Engineering", location: "Los Angeles, CA 🇺🇸", timePeriod: "Aug. 2025 - Present", }, { company: "National Central University", companyImage: "/images/logos/ncu.png", title: "Bachelors of Science in Atmospheric Science", employmentType: "College of Earth Sciences", location: "Taoyuan, Taiwan 🇹🇼", timePeriod: "Sep. 2020 - Jun. 2024", }, ], }, experience: { icon: Icons.Devices, title: "Professional Experiences", timeLines: [ // Experience entries... ], }, // Users can add more sections here } as ResumeData, } export default config
import type { VCardIconType } from "@/types/config" export type TimeLine = { company: string companyImage: string title: string employmentType: string location: string timePeriod: string } export type ResumeSection = { icon: VCardIconType title: string timeLines: TimeLine[] } export type ResumeData = { [key: string]: ResumeSection }
import React from "react"; import type { Metadata } from "next"; import PageHeader from "@/components/page-header"; import { Icons } from "@/components/icons"; import ResumeSection from "@/components/section/resume"; import config from "@/config"; import { IoSchoolOutline } from "react-icons/io5"; const { title } = config; const educations = { icon: IoSchoolOutline, title: "Education", sectionType: "education", timeLines: [ { company: "University of Southern California", companyImage: "/images/logos/usc.jpg", title: "Master of Science in Computer Science", employmentType: "Viterbi School of Engineering", location: "Los Angeles, CA 🇺🇸", timePeriod: "Aug. 2025 - Present", }, { company: "National Central University", companyImage: "/images/logos/ncu.png", title: "Bachelors of Science in Atmospheric Science", employmentType: "College of Earth Sciences", location: "Taoyuan, Taiwan 🇹🇼", timePeriod: "Sep. 2020 - Jun. 2024", } ] } const experiences = { icon: Icons.Devices, title: "Professional Experiences", sectionType: "experience", timeLines: [ { company: "eCloudvalley Digital Technology", companyImage: "/images/logos/ecv.png", title: "Cloud Engineer", employmentType: "Intern", location: "New Taipei, Taiwan 🇹🇼", timePeriod: "Mar. 2024 - May. 2024", }, { company: "Amazon Web Services", companyImage: "/images/logos/aws.svg", title: "Campus Ambassador", employmentType: "Contract", location: "Taipei, Taiwan 🇹🇼", timePeriod: "Aug. 2023 - Feb. 2024", }, { company: "PEGATRON", companyImage: "/images/logos/pegatron.png", title: "AI Engineer", employmentType: "Intern", location: "Taipei, Taiwan 🇹🇼", timePeriod: "Jul. 2023 - Aug. 2023", }, { company: "National Central University", companyImage: "/images/logos/ncu.png", title: "Website Developer", employmentType: "Part-time", location: "Taoyuan, Taiwan 🇹🇼", timePeriod: "Jul. 2022 - Jan. 2023", }, ], }; export const metadata: Metadata = { title: `Resume | ${title}`, }; export default function Resume() { return ( <article> <PageHeader header="Hugo's Resume" /> <ResumeSection icon={educations.icon} title={educations.title} timeLines={educations.timeLines} sectionType={educations.sectionType} /> <ResumeSection icon={experiences.icon} title={experiences.title} timeLines={experiences.timeLines} sectionType={experiences.sectionType} /> </article> ); } 這是我目前的做法, 我已經把資料定義到 config 裡了! export type Config = { resumeData: ResumeData; }; export type TimeLine = { company: string; companyImage: string; title: string; employmentType: string; location: string; timePeriod: string; }; export type ResumeSection = { icon: VCardIconType title: string timeLines: TimeLine[] } export type ResumeData = { [key: string]: ResumeSection } { resumeData: { education: { icon: IoSchoolOutline, title: "Education", sectionType: "education", timeLines: [ { company: "University of Southern California", companyImage: "/images/logos/usc.jpg", title: "Master of Science in Computer Science", employmentType: "Viterbi School of Engineering", location: "Los Angeles, CA 🇺🇸", timePeriod: "Aug. 2025 - Present", }, { company: "National Central University", companyImage: "/images/logos/ncu.png", title: "Bachelors of Science in Atmospheric Science", employmentType: "College of Earth Sciences", location: "Taoyuan, Taiwan 🇹🇼", timePeriod: "Sep. 2020 - Jun. 2024", } ] }, experiences: { icon: Icons.Devices, title: "Professional Experiences", sectionType: "experience", timeLines: [ { company: "eCloudvalley Digital Technology", companyImage: "/images/logos/ecv.png", title: "Cloud Engineer", employmentType: "Intern", location: "New Taipei, Taiwan 🇹🇼", timePeriod: "Mar. 2024 - May. 2024", }, { company: "Amazon Web Services", companyImage: "/images/logos/aws.svg", title: "Campus Ambassador", employmentType: "Contract", location: "Taipei, Taiwan 🇹🇼", timePeriod: "Aug. 2023 - Feb. 2024", }, { company: "PEGATRON", companyImage: "/images/logos/pegatron.png", title: "AI Engineer", employmentType: "Intern", location: "Taipei, Taiwan 🇹🇼", timePeriod: "Jul. 2023 - Aug. 2023", }, { company: "National Central University", companyImage: "/images/logos/ncu.png", title: "Website Developer", employmentType: "Part-time", location: "Taoyuan, Taiwan 🇹🇼", timePeriod: "Jul. 2022 - Jan. 2023", }, ], } }, } <ResumeSection icon={educations.icon} title={educations.title} timeLines={educations.timeLines} sectionType={educations.sectionType} /> <ResumeSection icon={experiences.icon} title={experiences.title} timeLines={experiences.timeLines} sectionType={experiences.sectionType} /> 請幫我改寫這兩個並要運用 resumeData
You're encountering a TypeScript error because when using `Object.entries(resumeData)`, TypeScript doesn't know the specific type of each entry. Let's fix this by adding proper type assertions: ```typescriptreact import React from "react"; import type { Metadata } from "next"; import PageHeader from "@/components/page-header"; import ResumeSection from "@/components/section/resume"; import config from "@/config"; import type { ResumeSection as ResumeSectionType } from "@/config"; const { title, resumeData } = config; export const metadata: Metadata = { title: `Resume | ${title}`, }; export default function Resume() { return ( <article> <PageHeader header="Hugo's Resume" /> {Object.entries(resumeData).map(([key, section]) => ( <ResumeSection key={key} icon={(section as ResumeSectionType).icon} title={(section as ResumeSectionType).title} timeLines={(section as ResumeSectionType).timeLines} sectionType={key} /> ))} </article> ); }

Alternatively, you can use a more type-safe approach by explicitly typing the entries:

import React from "react"; import type { Metadata } from "next"; import PageHeader from "@/components/page-header"; import ResumeSection from "@/components/section/resume"; import config from "@/config"; import type { ResumeSection as ResumeSectionType } from "@/config"; const { title, resumeData } = config; export const metadata: Metadata = { title: `Resume | ${title}`, }; export default function Resume() { return ( <article> <PageHeader header="Hugo's Resume" /> {(Object.entries(resumeData) as [string, ResumeSectionType][]).map(([key, section]) => ( <ResumeSection key={key} icon={section.icon} title={section.title} timeLines={section.timeLines} sectionType={key} /> ))} </article> ); }

This second approach is cleaner because it casts the entire result of Object.entries() to the expected type, rather than casting each property individually.

https://github.com/1chooo/1chooo.com/commit/0947fe70ba06ee32009a17db6d5a856de1370de0

Footnotes

  1. (react-scripts) Support for TypeScript 5.x #13080 2

  2. Generate a Web App Manifest with Next.js

Last updated on