├── .github
└── workflows
│ └── azure-static-web-apps-jolly-bay-0c013ea00.yml
├── .gitignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── bun.lockb
├── docs
├── .vitepress
│ ├── config.ts
│ ├── en.ts
│ ├── shared.ts
│ ├── th.ts
│ └── theme
│ │ ├── index.ts
│ │ └── style.css
├── basic-types
│ ├── basic-types.md
│ ├── generics.md
│ ├── literal-types.md
│ ├── record-object.md
│ ├── template-literal-types.md
│ └── tuple.md
├── design-guideline
│ ├── design-guideline.md
│ ├── project-structure.md
│ ├── type-safe-level.md
│ ├── when-type-safe.md
│ └── you-might-not-need-type-safe.md
├── design-patterns
│ ├── adapter-pattern.md
│ ├── builder-pattern.md
│ ├── design-patterns.md
│ ├── function-argument.md
│ ├── function-overload.md
│ └── loosen-and-tighten.md
├── examples.md
├── framework-pattern
│ ├── config-file.md
│ ├── framework-pattern.md
│ └── generate-dynamic-types.md
├── glossary.md
├── index.md
├── intro.md
├── performance
│ ├── performance.md
│ ├── typescript-performance-aleksandra.png
│ └── typescript-performance-going-beyond-the-surface.png
├── prompt.md
├── public
│ └── staticwebapp.config.json
├── th
│ ├── examples.md
│ ├── glossary.md
│ ├── index.md
│ ├── intro.md
│ ├── project-structure.md
│ ├── prompt.md
│ └── what-type-safe.md
├── type-programming
│ ├── conditional-types.md
│ ├── examples.md
│ ├── loop
│ │ ├── mapped-types.md
│ │ └── recursive-types.md
│ ├── testing.md
│ └── type-programming.md
└── what-type-safe.md
├── package.json
└── tsconfig.json
/.github/workflows/azure-static-web-apps-jolly-bay-0c013ea00.yml:
--------------------------------------------------------------------------------
1 | name: Azure Static Web Apps CI/CD
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build_and_deploy_job:
10 | runs-on: ubuntu-latest
11 | name: Build and Deploy Job
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: oven-sh/setup-bun@v1
15 |
16 | - name: Install dependencies
17 | run: bun install
18 |
19 | - name: Build
20 | run: bun run build
21 |
22 | - name: Deploy
23 | run: bun run deploy
24 | env:
25 | SWA_CLI_DEPLOYMENT_TOKEN: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_JOLLY_BAY_0C013EA00 }}
26 |
27 | # name: Azure Static Web Apps CI/CD
28 |
29 | # on:
30 | # push:
31 | # branches:
32 | # - main
33 | # pull_request:
34 | # types: [opened, synchronize, reopened, closed]
35 | # branches:
36 | # - main
37 |
38 | # jobs:
39 | # build_and_deploy_job:
40 | # if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
41 | # runs-on: ubuntu-latest
42 | # name: Build and Deploy Job
43 | # steps:
44 | # - uses: actions/checkout@v3
45 | # with:
46 | # submodules: true
47 | # lfs: false
48 | # - name: Build And Deploy
49 | # id: builddeploy
50 | # uses: Azure/static-web-apps-deploy@v1
51 | # with:
52 | # azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_JOLLY_BAY_0C013EA00 }}
53 | # repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
54 | # action: "upload"
55 | # ###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
56 | # # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
57 | # app_location: "/" # App source code path
58 | # api_location: "" # Api source code path - optional
59 | # output_location: "" # Built app content directory - optional
60 | # ###### End of Repository/Build Configurations ######
61 |
62 | # close_pull_request_job:
63 | # if: github.event_name == 'pull_request' && github.event.action == 'closed'
64 | # runs-on: ubuntu-latest
65 | # name: Close Pull Request Job
66 | # steps:
67 | # - name: Close Pull Request
68 | # id: closepullrequest
69 | # uses: Azure/static-web-apps-deploy@v1
70 | # with:
71 | # azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_JOLLY_BAY_0C013EA00 }}
72 | # action: "close"
73 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
2 |
3 | # Logs
4 |
5 | logs
6 | _.log
7 | npm-debug.log_
8 | yarn-debug.log*
9 | yarn-error.log*
10 | lerna-debug.log*
11 | .pnpm-debug.log*
12 |
13 | # Caches
14 |
15 | .cache
16 |
17 | # Diagnostic reports (https://nodejs.org/api/report.html)
18 |
19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
20 |
21 | # Runtime data
22 |
23 | pids
24 | _.pid
25 | _.seed
26 | *.pid.lock
27 |
28 | # Directory for instrumented libs generated by jscoverage/JSCover
29 |
30 | lib-cov
31 |
32 | # Coverage directory used by tools like istanbul
33 |
34 | coverage
35 | *.lcov
36 |
37 | # nyc test coverage
38 |
39 | .nyc_output
40 |
41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
42 |
43 | .grunt
44 |
45 | # Bower dependency directory (https://bower.io/)
46 |
47 | bower_components
48 |
49 | # node-waf configuration
50 |
51 | .lock-wscript
52 |
53 | # Compiled binary addons (https://nodejs.org/api/addons.html)
54 |
55 | build/Release
56 |
57 | # Dependency directories
58 |
59 | node_modules/
60 | jspm_packages/
61 |
62 | # Snowpack dependency directory (https://snowpack.dev/)
63 |
64 | web_modules/
65 |
66 | # TypeScript cache
67 |
68 | *.tsbuildinfo
69 |
70 | # Optional npm cache directory
71 |
72 | .npm
73 |
74 | # Optional eslint cache
75 |
76 | .eslintcache
77 |
78 | # Optional stylelint cache
79 |
80 | .stylelintcache
81 |
82 | # Microbundle cache
83 |
84 | .rpt2_cache/
85 | .rts2_cache_cjs/
86 | .rts2_cache_es/
87 | .rts2_cache_umd/
88 |
89 | # Optional REPL history
90 |
91 | .node_repl_history
92 |
93 | # Output of 'npm pack'
94 |
95 | *.tgz
96 |
97 | # Yarn Integrity file
98 |
99 | .yarn-integrity
100 |
101 | # dotenv environment variable files
102 |
103 | .env
104 | .env.development.local
105 | .env.test.local
106 | .env.production.local
107 | .env.local
108 |
109 | # parcel-bundler cache (https://parceljs.org/)
110 |
111 | .parcel-cache
112 |
113 | # Next.js build output
114 |
115 | .next
116 | out
117 |
118 | # Nuxt.js build / generate output
119 |
120 | .nuxt
121 | dist
122 |
123 | # Gatsby files
124 |
125 | # Comment in the public line in if your project uses Gatsby and not Next.js
126 |
127 | # https://nextjs.org/blog/next-9-1#public-directory-support
128 |
129 | # public
130 |
131 | # vuepress build output
132 |
133 | .vuepress/dist
134 |
135 | # vuepress v2.x temp and cache directory
136 |
137 | .temp
138 |
139 | # Docusaurus cache and generated files
140 |
141 | .docusaurus
142 |
143 | # Serverless directories
144 |
145 | .serverless/
146 |
147 | # FuseBox cache
148 |
149 | .fusebox/
150 |
151 | # DynamoDB Local files
152 |
153 | .dynamodb/
154 |
155 | # TernJS port file
156 |
157 | .tern-port
158 |
159 | # Stores VSCode versions used for testing VSCode extensions
160 |
161 | .vscode-test
162 |
163 | # yarn v2
164 |
165 | .yarn/cache
166 | .yarn/unplugged
167 | .yarn/build-state.yml
168 | .yarn/install-state.gz
169 | .pnp.*
170 |
171 | # IntelliJ based IDEs
172 | .idea
173 |
174 | # Finder (MacOS) folder config
175 | .DS_Store
176 |
177 | docs/.vitepress/cache
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "printWidth": 120
7 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International Public License
2 |
3 | This work by Thada Wangthammang, @2024 is licensed under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.
4 | You may copy and redistribute the material in any medium or format for non-commercial purposes only, provided proper attribution is given, but you may not distribute any modified material.
5 | For details, see the full license at https://creativecommons.org/licenses/by-nc-nd/4.0/
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Type-safe Design Pattern in Modern TypeScript
2 |
3 | This book provide ready to use design pattern for type-safe approach in modern typescript
4 | by Thada Wangthammang
5 |
6 | ## Table of Contents
7 |
8 | - TypeScript Config
9 | - Use strict
10 | - Data Structure
11 | - Use literal type rathen than string
12 | - Use Record object rather than list/array
13 | - Use Tuple rather than list/array
14 | - Design Patterns
15 | - Use builder pattern
16 | - Use function argument instead of plain object
17 | - Use function overload
18 |
19 | ## Prerequisites
20 | Please make sure you have a good understanding of TypeScript before reading this book. If you are new to TypeScript, I recommend reading the [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) first.
21 |
22 | Generics are a fundamental concept in TypeScript, so make sure you understand them. You can read the [Generics](https://www.typescriptlang.org/docs/handbook/2/generics.html) section of the TypeScript Handbook.
23 |
24 | After that, make sure you have understand type-level programming. You can read the [Type-Level Programming](https://type-level-typescript.com/) book.
25 | There are many free chapters available. However, I recommend buying the book to support the author.
26 |
27 | ## Disclaimer
28 |
29 | I did not create these design patterns. I learned from various modern TypeScript open-source projects, including famous ones like Zod, tRPC, Hono, Elysia, and many others, as well as writing the [Nammtham](https://nammatham.thaitype.dev/) (Azure Functions Framework). This process has helped me distill commonly used patterns.
30 |
31 | The type-safe design patterns is not suitable for every project. It is intended for projects that require high code quality and design. Make sure to evaluate whether these patterns are suitable for your project before using them.
32 |
33 | ## Contributing
34 | If you have any suggestions or feedback, please feel free to open an issue or submit a pull request on [GitHub](https://github.com/mildronize/type-safe-design-pattern)
35 |
36 | ## License
37 | This book is licensed under the [CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/)
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mildronize/type-safe-design-pattern/886a4ad916f84d60af2e88be68fb0acea8486465/bun.lockb
--------------------------------------------------------------------------------
/docs/.vitepress/config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitepress'
2 | import { shared } from './shared'
3 | import { en } from './en'
4 | import { th } from './th'
5 |
6 | export default defineConfig({
7 | ...shared,
8 | locales: {
9 | root: { label: 'English', ...en },
10 | th: { label: 'ภาษาไทย', ...th },
11 | }
12 | })
13 |
--------------------------------------------------------------------------------
/docs/.vitepress/en.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitepress";
2 | import { baseSidebar } from "./shared";
3 |
4 | export const enSidebar = baseSidebar.clone().toSidebarItems();
5 | // console.log(JSON.stringify(enSidebar, null, 2));
6 |
7 | // https://vitepress.dev/reference/site-config
8 | export const en = defineConfig({
9 | lang: "en-US",
10 | themeConfig: {
11 | // https://vitepress.dev/reference/default-theme-config
12 | nav: [
13 | { text: "Home", link: "/" },
14 | { text: "Book", link: "/intro" },
15 | ],
16 |
17 | sidebar: enSidebar,
18 |
19 | footer: {
20 | message:
21 | 'Content License under CC BY-NC-ND 4.0',
22 | copyright: `Copyright © 2024-${new Date().getFullYear()} Thada Wangthammang`,
23 | },
24 |
25 | editLink: {
26 | pattern: "https://github.com/mildronize/type-safe-design-pattern/tree/main/docs/:path",
27 | },
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/docs/.vitepress/shared.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, type HeadConfig } from "vitepress";
2 | import { transformerTwoslash } from "@shikijs/vitepress-twoslash";
3 | import { Sidebar } from "@thaitype/vitepress-typed-navbar";
4 |
5 | export const shared = defineConfig({
6 | lastUpdated: true,
7 | title: "Type-safe Design Pattern",
8 | description: "Ready to use design Pattern for type-safe approach in modern typescript",
9 | markdown: {
10 | codeTransformers: [transformerTwoslash()],
11 | },
12 |
13 | themeConfig: {
14 | socialLinks: [
15 | {
16 | icon: "github",
17 | link: "https://github.com/mildronize/type-safe-design-pattern",
18 | },
19 | ],
20 | search: {
21 | provider: "local",
22 | },
23 | },
24 |
25 | head: [...googleFonts(), ...googleAnalytics("G-LWNNLXVF0K")],
26 | });
27 |
28 | /**
29 | * Add Google Analytics
30 | * @ref https://vitepress.dev/reference/site-config#example-using-google-analytics
31 | * @param tagManagerId
32 | * @returns
33 | */
34 | function googleAnalytics(tagManagerId: string): HeadConfig[] {
35 | return [
36 | ["script", { async: "", src: `https://www.googletagmanager.com/gtag/js?id=${tagManagerId}` }],
37 | [
38 | "script",
39 | {},
40 | `window.dataLayer = window.dataLayer || [];
41 | function gtag(){dataLayer.push(arguments);}
42 | gtag('js', new Date());
43 | gtag('config', '${tagManagerId}');`,
44 | ],
45 | ];
46 | }
47 |
48 | function googleFonts(): HeadConfig[] {
49 | return [
50 | ["link", { rel: "preconnect", href: "https://fonts.googleapis.com" }],
51 | ["link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "" }],
52 | [
53 | "link",
54 | { href: "https://fonts.googleapis.com/css2?family=Noto+Sans+Thai:wght@100..900&display=swap", rel: "stylesheet" },
55 | ],
56 | ];
57 | }
58 |
59 | export const baseSidebar = new Sidebar({
60 | collapsed: true,
61 | extraMessage: "🚧",
62 | })
63 | /**
64 | * Start Reading Section
65 | */
66 | .addGroup("/", { text: "Start Reading" })
67 | .add("/", "intro", { text: "Introduction", link: "/intro" })
68 | .add("/", "type-safe", { text: "What is Type-safe", link: "/what-type-safe" })
69 | .add("/", "glossary", { text: "Glossary", link: "/glossary" })
70 | .add("/", "examples", { text: "Examples", link: "/examples" })
71 | /**
72 | * Design Guideline Section
73 | */
74 | .addGroup("/design-guideline", { text: "Design Guideline" })
75 | .add("/design-guideline", "design-guideline", {
76 | text: "Intro",
77 | link: "/design-guideline",
78 | docFooterText: "Intro to Design Guideline",
79 | })
80 | .add("/design-guideline", "when-type-safe", { text: "When Type-safe", link: "/when-type-safe" })
81 | .add("/design-guideline", "type-safe-level", { text: "Type-Safe Level", link: "/type-safe-level" })
82 | .add("/design-guideline", "project-structure", { text: "Project Structure", link: "/project-structure" })
83 | .add('/design-guideline', 'you-might-not-need-type-safe', { text: "You Might Not Need Type-safe", link: "/you-might-not-need-type-safe" })
84 | /**
85 | * Basic Types Section
86 | */
87 | .addGroup("/basic-types", { text: "Basic Types" })
88 | .add("/basic-types", "basic-types", {
89 | text: "Intro",
90 | link: "/basic-types",
91 | docFooterText: "Intro to Basic Types",
92 | })
93 | .add("/basic-types", "literal-types", { text: "Literal Types", link: "/literal-types" })
94 | .add("/basic-types", "template-literal-types", {
95 | text: "Template Literal Types",
96 | link: "/template-literal-types",
97 | })
98 | .add("/basic-types", "tuple", { text: "Tuple Types", link: "/tuple" })
99 | .add("/basic-types", "record-object", { text: "Record Types", link: "/record-object" })
100 | .add("/basic-types", "generics", { text: "Generics", link: "/generics" })
101 | /**
102 | * Type Programming Section
103 | */
104 | .addGroup("/type-programming", { text: "Type Programming" })
105 | .add("/type-programming", "type-programming", {
106 | text: "Intro",
107 | link: "/type-programming",
108 | docFooterText: "Intro to Type Programming",
109 | })
110 | .add("/type-programming", "conditional-types", { text: "Conditional Types", link: "/conditional-types" })
111 |
112 | .addGroup("/type-programming/loop", { text: "Loop" })
113 | .add("/type-programming/loop", "mapped-types", { text: "Mapped Types", link: "/mapped-types" })
114 | .add("/type-programming/loop", "recursive-types", { text: "Recursive Types", link: "/recursive-types" })
115 | .add("/type-programming", "testing", { text: "Testing", link: "/testing" })
116 | .add("/type-programming", "examples", { text: "Examples", link: "/examples" })
117 | /**
118 | * Design Patterns Section
119 | */
120 | .addGroup("/design-patterns", { text: "Design Patterns" })
121 | .add("/design-patterns", "design-patterns", {
122 | text: "Intro",
123 | link: "/design-patterns",
124 | docFooterText: "Intro to Design Patterns",
125 | })
126 | .add("/design-patterns", "loosen-and-tighten", { text: "Loosen and Tighten", link: "/loosen-and-tighten" })
127 | .add("/design-patterns", "builder-pattern", { text: "Builder Pattern", link: "/builder-pattern" })
128 | .add("/design-patterns", "function-overload", { text: "Function Overloading", link: "/function-overload" })
129 | .add("/design-patterns", "function-argument", { text: "Function Argument", link: "/function-argument" })
130 | .add("/design-patterns", "adapter-pattern", {text: "Adapter Pattern", link: "/adapter-pattern"})
131 | /**
132 | * Framework Patterns Section
133 | */
134 | .addGroup("/framework-pattern", { text: "Framework Patterns" })
135 | .add("/framework-pattern", "framework-pattern", {
136 | text: "Intro",
137 | link: "/framework-pattern",
138 | docFooterText: "Intro to Framework Pattern",
139 | })
140 | .add("/framework-pattern", "config-file", { text: "Config File", link: "/config-file" })
141 | .add("/framework-pattern", "generate-dynamic-types", {
142 | text: "Generate Dynamic Types",
143 | link: "/generate-dynamic-types",
144 | })
145 | /**
146 | * Performance Section
147 | */
148 | .addGroup("/performance", { text: "Performance" })
149 | .add("/performance", "performance", {
150 | text: "Intro",
151 | link: "/performance",
152 | docFooterText: "Intro to Performance",
153 | });
154 |
--------------------------------------------------------------------------------
/docs/.vitepress/th.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitepress";
2 | import { baseSidebar } from "./shared";
3 |
4 | const prefix = "/th";
5 | /**
6 | * Note: Use prefix only tranlated page, the page is not translated will remove the link.
7 | */
8 | export const thSidebar = baseSidebar
9 | .clone()
10 | .overrideGroup("/", { text: "เริ่มต้นอ่าน" })
11 | .override("/intro", { text: "บทนำ", prefix })
12 | .override("/type-safe", { text: "ความหมายของ Type-safe", prefix })
13 | .override("/glossary", { text: "คลังคำศัพท์", prefix })
14 | .override("/examples", { text: "ตัวอย่าง", prefix })
15 | .overrideGroup("/design-guideline", { text: "แนวทางการออกแบบ" })
16 | .override("/design-guideline/design-guideline", { text: "บทนำ" })
17 | .override("/design-guideline/when-type-safe", { text: "เมื่อไหร่ควรใช้ Type-safe" })
18 | .override("/design-guideline/project-structure", { text: "โครงสร้างโปรเจกต์", prefix })
19 | .overrideGroup("/basic-types", { text: "โครงสร้างข้อมูล" })
20 | .override("/basic-types/basic-types", { text: "บทนำ" })
21 | .override("/basic-types/literal-types", {})
22 | .override("/basic-types/tuple", {})
23 | .override("/basic-types/record-object", {})
24 | .override("/basic-types/template-literal-types", {})
25 | .overrideGroup("/type-programming", { text: "เขียนโปรแกรมด้วย Type" })
26 | .override("/type-programming/type-programming", { text: "บทนำ" })
27 | .override("/type-programming/conditional-types", { text: "เงื่อนไข (Conditional Types)" })
28 | .overrideGroup("/type-programming/loop", { text: "การวนลูป" })
29 | .override("/type-programming/loop/mapped-types", {})
30 | .override("/type-programming/loop/recursive-types", {})
31 | .override("/type-programming/testing", { text: "การทดสอบ" })
32 | .override("/type-programming/examples", { text: "ตัวอย่าง" })
33 | .overrideGroup("/design-patterns", { text: "รูปแบบการออกแบบ" })
34 | .override("/design-patterns/design-patterns", { text: "บทนำ" })
35 | .override("/design-patterns/loosen-and-tighten", { text: "ทำให้หลวมและแน่นขึ้น" })
36 | .override("/design-patterns/builder-pattern", { text: "รูปแบบ Builder Pattern" })
37 | .override("/design-patterns/function-overload", { text: "การใช้ Function หลายรูปแบบ" })
38 | .override("/design-patterns/function-argument", { text: "การใช้ Argument ชนิดข้อมูลเป็นฟังก์ชัน" })
39 | .overrideGroup("/framework-pattern", { text: "การออกแบบของ Framework" })
40 | .override("/framework-pattern/framework-pattern", { text: "บทนำ" })
41 | .override("/framework-pattern/config-file", { text: "การใช้ไฟล์สำหรับการตั้งค่า" })
42 | .override("/framework-pattern/generate-dynamic-types", { text: "การสร้างชนิดข้อมูลแบบยืดหยุ่น" })
43 | .overrideGroup("/performance", { text: "ประสิทธิภาพ" })
44 | .override("/performance/performance", { text: "บทนำ" })
45 | .toSidebarItems();
46 |
47 | export const th = defineConfig({
48 | lang: "th",
49 |
50 | description: "รูปแบบการออกแบบพร้อมใช้งานโดยมีชนิดข้อมูลที่ปลอดภัยใน typescript สมัยใหม่",
51 |
52 | themeConfig: {
53 | outline: {
54 | label: "สารบัญ",
55 | },
56 | docFooter: {
57 | next: "หน้าถัดไป",
58 | prev: "หน้าก่อนหน้า",
59 | },
60 | lastUpdated: {
61 | text: "อัพเดทล่าสุดเมื่อวันที่",
62 | formatOptions: {
63 | timeZone: "Asia/Bangkok",
64 | day: "numeric",
65 | month: "long",
66 | year: "numeric",
67 | forceLocale: true,
68 | hour: "numeric",
69 | minute: "numeric",
70 | },
71 | },
72 | nav: [
73 | { text: "หน้าแรก", link: prefix + "/" },
74 | { text: "หนังสือ", link: prefix + "/intro" },
75 | ],
76 | sidebar: thSidebar,
77 |
78 | footer: {
79 | message:
80 | 'เนื้อหาอยู่ภายใต้ใบอนุญาต CC BY-NC-ND 4.0',
81 | copyright: `ลิขสิทธิ์ © 2567-${new Date().getFullYear() + 543} ธาดา หวังธรรมมั่ง`,
82 | },
83 |
84 | editLink: {
85 | pattern: "https://github.com/mildronize/type-safe-design-pattern/tree/main/docs/:path",
86 | text: "แก้ไขหน้านี้บน GitHub",
87 | },
88 | },
89 | });
90 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.ts:
--------------------------------------------------------------------------------
1 | // https://vitepress.dev/guide/custom-theme
2 | import { h } from "vue";
3 | import type { Theme } from "vitepress";
4 | import TwoslashFloatingVue from "@shikijs/vitepress-twoslash/client";
5 | import "@shikijs/vitepress-twoslash/style.css";
6 | import type { EnhanceAppContext } from "vitepress";
7 | import DefaultTheme from "vitepress/theme";
8 | import "./style.css";
9 |
10 | export default {
11 | extends: DefaultTheme,
12 | Layout: () => {
13 | return h(DefaultTheme.Layout, null, {
14 | // https://vitepress.dev/guide/extending-default-theme#layout-slots
15 | });
16 | },
17 | enhanceApp({ app, router, siteData }: EnhanceAppContext) {
18 | app.use(TwoslashFloatingVue);
19 | },
20 | } satisfies Theme;
21 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/style.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Customize default theme styling by overriding CSS variables:
3 | * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
4 | */
5 |
6 | /**
7 | * Colors
8 | *
9 | * Each colors have exact same color scale system with 3 levels of solid
10 | * colors with different brightness, and 1 soft color.
11 | *
12 | * - `XXX-1`: The most solid color used mainly for colored text. It must
13 | * satisfy the contrast ratio against when used on top of `XXX-soft`.
14 | *
15 | * - `XXX-2`: The color used mainly for hover state of the button.
16 | *
17 | * - `XXX-3`: The color for solid background, such as bg color of the button.
18 | * It must satisfy the contrast ratio with pure white (#ffffff) text on
19 | * top of it.
20 | *
21 | * - `XXX-soft`: The color used for subtle background such as custom container
22 | * or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
23 | * on top of it.
24 | *
25 | * The soft color must be semi transparent alpha channel. This is crucial
26 | * because it allows adding multiple "soft" colors on top of each other
27 | * to create a accent, such as when having inline code block inside
28 | * custom containers.
29 | *
30 | * - `default`: The color used purely for subtle indication without any
31 | * special meanings attched to it such as bg color for menu hover state.
32 | *
33 | * - `brand`: Used for primary brand colors, such as link text, button with
34 | * brand theme, etc.
35 | *
36 | * - `tip`: Used to indicate useful information. The default theme uses the
37 | * brand color for this by default.
38 | *
39 | * - `warning`: Used to indicate warning to the users. Used in custom
40 | * container, badges, etc.
41 | *
42 | * - `danger`: Used to show error, or dangerous message to the users. Used
43 | * in custom container, badges, etc.
44 | * -------------------------------------------------------------------------- */
45 |
46 | :root {
47 | --vp-c-default-1: var(--vp-c-gray-1);
48 | --vp-c-default-2: var(--vp-c-gray-2);
49 | --vp-c-default-3: var(--vp-c-gray-3);
50 | --vp-c-default-soft: var(--vp-c-gray-soft);
51 |
52 | --vp-c-brand-1: var(--vp-c-indigo-1);
53 | --vp-c-brand-2: var(--vp-c-indigo-2);
54 | --vp-c-brand-3: var(--vp-c-indigo-3);
55 | --vp-c-brand-soft: var(--vp-c-indigo-soft);
56 |
57 | --vp-c-tip-1: var(--vp-c-brand-1);
58 | --vp-c-tip-2: var(--vp-c-brand-2);
59 | --vp-c-tip-3: var(--vp-c-brand-3);
60 | --vp-c-tip-soft: var(--vp-c-brand-soft);
61 |
62 | --vp-c-warning-1: var(--vp-c-yellow-1);
63 | --vp-c-warning-2: var(--vp-c-yellow-2);
64 | --vp-c-warning-3: var(--vp-c-yellow-3);
65 | --vp-c-warning-soft: var(--vp-c-yellow-soft);
66 |
67 | --vp-c-danger-1: var(--vp-c-red-1);
68 | --vp-c-danger-2: var(--vp-c-red-2);
69 | --vp-c-danger-3: var(--vp-c-red-3);
70 | --vp-c-danger-soft: var(--vp-c-red-soft);
71 | }
72 |
73 | /**
74 | * Component: Button
75 | * -------------------------------------------------------------------------- */
76 |
77 | :root {
78 | --vp-button-brand-border: transparent;
79 | --vp-button-brand-text: var(--vp-c-white);
80 | --vp-button-brand-bg: var(--vp-c-brand-3);
81 | --vp-button-brand-hover-border: transparent;
82 | --vp-button-brand-hover-text: var(--vp-c-white);
83 | --vp-button-brand-hover-bg: var(--vp-c-brand-2);
84 | --vp-button-brand-active-border: transparent;
85 | --vp-button-brand-active-text: var(--vp-c-white);
86 | --vp-button-brand-active-bg: var(--vp-c-brand-1);
87 | }
88 |
89 | /**
90 | * Component: Home
91 | * -------------------------------------------------------------------------- */
92 |
93 | /* Origin Theme: Blue */
94 |
95 | /* :root {
96 | --vp-home-hero-name-color: transparent;
97 | --vp-home-hero-name-background: -webkit-linear-gradient(
98 | 120deg,
99 | #bd34fe 30%,
100 | #41d1ff
101 | );
102 |
103 | --vp-home-hero-image-background-image: linear-gradient(
104 | -45deg,
105 | #bd34fe 50%,
106 | #47caff 50%
107 | );
108 | --vp-home-hero-image-filter: blur(44px);
109 | } */
110 |
111 | /* Deep Ocean Gradient */
112 |
113 | :root {
114 | --vp-home-hero-name-color: transparent;
115 | --vp-home-hero-name-background: -webkit-linear-gradient(120deg, #006994 30%, /* deep blue */ #00bcd4 /* turquoise */);
116 |
117 | --vp-home-hero-image-background-image: linear-gradient(-45deg, #006994 50%, /* deep blue */ #00bcd4 /* turquoise */);
118 | --vp-home-hero-image-filter: blur(44px);
119 | }
120 |
121 | @media (min-width: 640px) {
122 | :root {
123 | --vp-home-hero-image-filter: blur(56px);
124 | }
125 | }
126 |
127 | @media (min-width: 960px) {
128 | :root {
129 | --vp-home-hero-image-filter: blur(68px);
130 | }
131 | }
132 |
133 | /**
134 | * Component: Custom Block
135 | * -------------------------------------------------------------------------- */
136 |
137 | :root {
138 | --vp-custom-block-tip-border: transparent;
139 | --vp-custom-block-tip-text: var(--vp-c-text-1);
140 | --vp-custom-block-tip-bg: var(--vp-c-brand-soft);
141 | --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
142 | }
143 |
144 | /**
145 | * Component: Algolia
146 | * -------------------------------------------------------------------------- */
147 |
148 | .DocSearch {
149 | --docsearch-primary-color: var(--vp-c-brand-1) !important;
150 | }
151 |
152 |
153 | /**
154 | * Custom CSS
155 | * -------------------------------------------------------------------------- */
156 |
157 | :root {
158 | --vp-font-family-base: "Noto Sans Thai", sans-serif;
159 | }
--------------------------------------------------------------------------------
/docs/basic-types/basic-types.md:
--------------------------------------------------------------------------------
1 | # Basic Types
2 |
3 | Data Structures are a way of organizing and storing data so that they can be accessed and worked with efficiently. They define the relationship between the data, and the operations that can be performed on the data. There are many different types of data structures, generally built upon simpler primitive data types.
4 |
5 | In TypeScript, data structures are used to represent data in a structured way. They can be used to store data in a way that makes it easy to access and manipulate. TypeScript provides several built-in data structures, such as arrays, tuples, and objects, as well as the ability to define custom data structures using interfaces and classes.
6 |
7 | In order to create type-safe data structures in TypeScript, it is important to understand the different types of data structures available, how they work, and when to use them. This guide will provide an overview of some of the most common data structures in TypeScript, and how to use them effectively in your code.
8 |
9 | Not all data structures are type-safe, but some of them are very useful in TypeScript. Here are some data structures that are commonly used in TypeScript:
10 | - Use [literal type](./literal-types) rathen than string
11 | - Use [Record object](./record-object.md) rather than list/array
12 | - Use [Tuple](./tuple.md) rather than list/array
--------------------------------------------------------------------------------
/docs/basic-types/generics.md:
--------------------------------------------------------------------------------
1 | # Generics
2 |
3 | ## Basic Generics
4 |
5 | TBA...
6 |
7 | ## Understanding Type Inference from Function Arguments
8 |
9 | When you pass a function as an argument, TypeScript will infer the type of the function based on the arguments passed to it. This is known as type inference.
10 |
11 | ```ts twoslash
12 | function identity(arg: T): T {
13 | return arg;
14 | }
15 |
16 | identity(42); // number
17 | // ^?
18 |
19 | // End of Example
20 | ```
21 |
22 | In the above example, TypeScript infers the type of `T` as `number` because the argument passed to the `identity` function is a number.
23 |
24 | ### Think Generic Param as a variable
25 |
26 | Generics are like variables for types. When you define a generic type, you are defining a placeholder for a type that will be determined when the generic type is used.
27 |
28 | Defining generic types is similar to defining variables, you can use in every place where it is in scope. For example below, `Path` is a generic type that can be used in the function signature and return type.
29 |
30 | ```ts twoslash
31 | type Paths = {
32 | "/": "home";
33 | "/about": "about";
34 | "/users": "users";
35 | };
36 |
37 | function routeTo(path: Path) {
38 | return path as Path;
39 | }
40 |
41 | routeTo("/users");
42 | //^?
43 |
44 | // End of Example
45 | ```
46 |
47 | Moreover, you can use generic with any type features such as template literal types.
48 | If I want to function that recieve one more argument and return a string that is a combination of the two arguments, I can use template literal types with generics.
49 |
50 | ```ts twoslash
51 | type Paths = {
52 | "/": "home";
53 | "/about": "about";
54 | "/users": "users";
55 | };
56 |
57 | function routeTo<
58 | Path extends keyof Paths,
59 | Param extends string
60 | >(path: Path, params: Param) {
61 | return `${path}/${params}` as `${Path}/${Param}`;
62 | }
63 |
64 | routeTo("/users", '324');
65 | // ^?
66 |
67 | // End of Example
68 | ```
69 |
70 | For example, the `routeTo` function takes two arguments: `path` and `params`. The `path` argument is a key of the `Paths` type, and the `params` argument is a string. The function returns a string that is a combination of the two arguments. So, the return type will be `"/users/324"`.
--------------------------------------------------------------------------------
/docs/basic-types/literal-types.md:
--------------------------------------------------------------------------------
1 | # Literal Types
2 |
3 | In TypeScript, a literal string is a string that has a specific value. For example, the string `"hello"` is a literal string with the value `hello`.
4 |
5 | ## Prerequisites
6 | - [Literal types are SO USEFUL in TS - Advanced TypeScript](https://www.youtube.com/watch?v=jxSh-RZTCws) by [Matt Pocock](https://x.com/mattpocockuk)
7 | - [Literal Type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types)
--------------------------------------------------------------------------------
/docs/basic-types/record-object.md:
--------------------------------------------------------------------------------
1 | # Record Object
2 |
3 | In TypeScript, a record object is a type that represents an object with keys of type `string` and values of type `unknown`. The `Record` type is a built-in utility type that allows you to define a record object with specific keys and values.
4 |
5 | ```ts twoslash
6 | type User = Record;
7 | ```
8 |
9 | Mostly use with [builder pattern](../design-patterns//builder-pattern) to construct complex objects step by step.
10 |
11 | ## Using Record Object for Enhanced Type Safety
12 |
13 | Arrays in TypeScript are not inherently type-safe. For example:
14 |
15 | ```ts
16 | type Tenant = {
17 | name: string;
18 | metadata: Record;
19 | }[];
20 |
21 | ```
22 |
23 | In this case, we cannot use literal types with arrays. However, if we structure it as an object:
24 |
25 | ```ts
26 | type Tenant = Record<'Provider' | 'Consumer', {
27 | metadata: Record
28 | }>;
29 | ```
30 |
31 | We can infer types from the keys, which are literals.
32 |
33 | ### Flexibility vs. Type Safety
34 |
35 | If we require the flexibility of an array—such as when managing tenants with varying roles like provider, consumer, or potentially new roles in the future—we might still use an array. However, this approach lacks type safety, necessitating manual type checks and validations.
36 |
37 | Conversely, using an object instead of an array ensures type safety. The challenge, however, is that TypeScript, being a statically typed language, does not allow us to derive literal types from runtime data. As a result, many developers opt to auto-generate type definitions (`.d.ts` files) to maintain type safety and reflect any configuration changes in the code.
38 |
39 | This technique will be further explained in the chapter on Framework Patterns, specifically in the section on Generating Dynamic Types.
40 |
--------------------------------------------------------------------------------
/docs/basic-types/template-literal-types.md:
--------------------------------------------------------------------------------
1 | # Template Literal Types
2 |
3 | In TypeScript, template literal types are a powerful feature that allows you to create types by combining string literals and expressions. Template literal types are used to create types that depend on the values of other types. They are often used in generic types to create more flexible and reusable code.
4 |
5 |
6 | ## Basic Usage
7 |
8 | TBA...
9 |
10 |
11 |
12 | Checkout [TypeScript Awesome Template Literal Types](https://github.com/ghoullier/awesome-template-literal-types) for more resources.
--------------------------------------------------------------------------------
/docs/basic-types/tuple.md:
--------------------------------------------------------------------------------
1 | # Tuple
2 |
3 | In TypeScript, a tuple is a type that represents an array with a fixed number of elements. The elements of a tuple can have different types, and the order of the elements is fixed.
4 |
5 | ```ts twoslash
6 | type Point = [number, number];
7 | const point: Point = [10, 20];
8 | ```
--------------------------------------------------------------------------------
/docs/design-guideline/design-guideline.md:
--------------------------------------------------------------------------------
1 | # Design Guideline
--------------------------------------------------------------------------------
/docs/design-guideline/project-structure.md:
--------------------------------------------------------------------------------
1 | # Project Setup and Structure
2 |
3 | ## TypeScript Compiler Config
4 |
5 | The TypeScript compiler configuration file is `tsconfig.json`. This file is used to configure the TypeScript compiler. The TypeScript compiler uses this file to determine how to compile the project. Use `strict` mode to enable all strict type-checking options. This will help you catch more errors at compile time.
6 |
7 | ```json
8 | {
9 | "compilerOptions": {
10 | "strict": true
11 | }
12 | }
13 | ```
14 |
15 | ## Project Structure
16 |
17 | ::: warning Disclaimer
18 | Organizing the project structure and types may vary depending on the project's size and complexity. From my experience, the following project structure design works well for most projects. However, the specific needs and context of a project might necessitate deviations from these practices. Flexibility and critical thinking are key in effectively applying these principles.
19 | :::
20 |
21 | ### Module Organization
22 |
23 | **Modular Approach**: I typically organize my code and types in a modular fashion. This means creating smaller, self-contained modules rather than having everything in a single file. Functions or classes that work together frequently are placed in the same module.
24 |
25 | This approach aligns with the Single Responsibility Principle (SRP) from SOLID principles, which encourages developers to structure their code such that each module or file has a single responsibility.
26 |
27 | ### Type Definition Organization
28 |
29 | **Inline Types**: Types that are specific to a module (e.g., parameters for functions or classes) are defined within that module. This ensures that related types are close to the code that uses them. Because it makes the code more readable and easier to understand. This is particularly useful for types that are specific to a particular module and not reused elsewhere.
30 |
31 | **Shared Types**: For types that need to be used across multiple classes, I create a separate file, usually named `types.ts`, within the same module. This file contains shared interfaces or utility types specific to that module (domain strictly). This prevents type definitions from cluttering the main module file and keeps related types organized.
32 |
33 | **Common/Global Types**: There are also common types used across all modules, such as utility types that are not specific to any business logic or module. These are placed in a common directory, making them accessible to any part of the project. This ensures that such utility types are easily accessible and do not need to be duplicated.
34 |
35 | ### Type Declaration Files
36 |
37 | I recommend avoiding the use of `.d.ts` files unless absolutely necessary. They should be used sparingly, as global declarations can be difficult to manage and maintain as the project grows. Additionally, they can lead to discrepancies between compile-time and runtime types, causing more complexity. Instead, define types within modules and use import paths or wildcard imports (`import * as types from ...`) to manage type imports efficiently.
38 |
39 | However, `.d.ts` files can be very useful for defining global types, especially when working with third-party libraries or when extending built-in types. Completely avoiding `.d.ts` files might not be practical in all scenarios. It’s important to balance their usage with careful management to avoid issues.
40 |
41 | ### Summary
42 |
43 | The principles for organizing types are similar to those for organizing JavaScript code: separate by domain, module, and business logic. If you're using a type-safe framework, the need to write type definitions may be significantly reduced, as these frameworks often infer types automatically. In such cases, you mainly need to define data interfaces to complement the framework's type inference.
44 |
45 | This approach ensures a clean, maintainable project structure that scales well as the project grows.
--------------------------------------------------------------------------------
/docs/design-guideline/type-safe-level.md:
--------------------------------------------------------------------------------
1 | # Type-safe Level
2 |
3 | ::: tip TL;DR:
4 | **Type-safe level** is a measure of how much a project is type-safe. It is a subjective measure and can be different from one person to another. However, it is important to have a common understanding of what it means to be type-safe.
5 | :::
6 |
7 | TypeScript's type-safety stands out as the most robust I've encountered in any programming language, a point I'm elaborating on in my book under the topic of "Type-safe Level."
8 |
9 | One of TypeScript's significant advantages is its ability to define types on the server side and seamlessly transfer them to the client side at compile time. This eliminates the need for an intermediate build process between client and server, as shown by the real-world use case of tRPC.
10 |
11 | Consider a scenario where I set up a .NET project, export types from .NET to OpenAPI, and then generate these types into TypeScript. Is this considered type-safe? According to my personal definition, it is a form of type-safety, although it still depends on an additional build process. This will be discussed in the Framework Pattern section, specifically under [generating dynamic types](../framework-pattern/generate-dynamic-types), although this part is still a work in progress.
12 |
13 | In contrast, TypeScript allows for immediate type-checking without the need for an extra compiler step. The editor's IntelliSense recognizes the types as you write, providing instant feedback and enhancing the development experience.
14 |
15 | A feature that sets TypeScript apart is its Literal Template Strings at the type level, introduced in TypeScript 4.1 at the end of 2020. This feature marks a significant milestone, which I refer to as the advent of Modern TypeScript. It allows for advanced type inference through patterns like the Builder Pattern, achieving an exceptional level of type-safety, as seen in projects like tRPC and Elysia. While the Builder Pattern predates TypeScript 4.1, its application in conjunction with Literal Template Strings has elevated TypeScript's type-safety capabilities.
16 |
17 | It’s important to note that this discussion is not intended to criticize the type-safety of other languages. Many languages achieve type-safety during compile time, and even memory-safe languages like Rust follow this approach. However, TypeScript's unique features and real-time type-checking capabilities provide a distinct and powerful development experience.
18 |
19 | ## Type-safe Level in TypeScript
20 |
21 | TBA...
22 |
--------------------------------------------------------------------------------
/docs/design-guideline/when-type-safe.md:
--------------------------------------------------------------------------------
1 | # When Type-safe?
2 |
3 | Not all projects need to be type-safe. However, if you are working on a project that requires high reliability, correctness, and maintainability, you should consider using type-safe design patterns.
4 |
5 | TBA...
--------------------------------------------------------------------------------
/docs/design-guideline/you-might-not-need-type-safe.md:
--------------------------------------------------------------------------------
1 | # You Might Not Need Type-safe
--------------------------------------------------------------------------------
/docs/design-patterns/adapter-pattern.md:
--------------------------------------------------------------------------------
1 | # Adapter Pattern
2 |
3 | In software design, the Adapter Pattern allows incompatible interfaces to work together by converting the interface of a class into another interface expected by the client. This pattern is especially useful when integrating with third-party services or legacy systems. It helps decouple the client code from the specifics of a service by using an abstraction, making the system more flexible and easier to maintain.
4 |
5 | ## Basic Adapter Pattern
6 |
7 | Let’s consider a scenario where we have different payment providers like PayPal and Stripe. Each provider has its own implementation, but we want a unified interface to interact with them so that our application doesn't need to change depending on the provider.
8 |
9 | In this case, we can use the Adapter Pattern to create a common interface for payments, and then implement specific adapters for PayPal and Stripe.
10 |
11 | ### Step 1: Define a Common Interface
12 |
13 | The first step is to define a common interface for payments. In our example, the `PaymentAdapterBase` interface ensures that every payment method we implement follows the same contract.
14 |
15 | ```ts
16 | export interface PaymentAdapterBase {
17 | pay(amount: number): void;
18 | }
19 | ```
20 |
21 | This interface defines a `pay` method that accepts an `amount` parameter. Any class that implements this interface must provide its own logic for how the payment is processed.
22 |
23 | ### Step 2: Create Adapters for Payment Providers
24 |
25 | We can now create adapter classes for PayPal and Stripe. Each class implements the `PaymentAdapterBase` interface and provides its own implementation of the `pay` method.
26 |
27 | ```ts
28 | export class PayPalAdapter implements PaymentAdapterBase {
29 | pay(amount: number): void {
30 | console.log(`Pay amount ${amount}$ with PayPal`);
31 | }
32 | }
33 |
34 | export class StripeAdapter implements PaymentAdapterBase {
35 | pay(amount: number): void {
36 | console.log(`Pay amount ${amount}$ with Stripe`);
37 | }
38 | }
39 | ```
40 |
41 | In this example, `PayPalAdapter` and `StripeAdapter` each have their own way of handling payments. However, they both adhere to the same interface (`PaymentAdapterBase`), ensuring consistency.
42 |
43 | ### Step 3: Use the PaymentProcessor
44 |
45 | To simplify how we handle payments, we create a `PaymentProcessor` class that accepts any implementation of `PaymentAdapterBase`. This allows us to change payment providers without modifying the client code.
46 |
47 | ```ts
48 | class PaymentProcessor {
49 | constructor(protected readonly payment: PaymentAdapterBase) { }
50 |
51 | pay(amount: number) {
52 | this.payment.pay(amount);
53 | }
54 | }
55 | ```
56 |
57 | Here, `PaymentProcessor` uses dependency injection to accept any `PaymentAdapterBase` implementation. This way, you can swap out payment providers without changing the logic of the `PaymentProcessor` class itself.
58 |
59 | ### Step 4: Making a Payment
60 |
61 | Now that we have everything set up, we can use the `PaymentProcessor` to process payments with different providers.
62 |
63 | ```ts
64 | const payment = new PaymentProcessor(new PayPalAdapter());
65 | payment.pay(100);
66 | ```
67 |
68 | In this example, the `PaymentProcessor` uses the `PayPalAdapter` to process the payment of `$100`. If we want to switch to Stripe, we can easily replace `PayPalAdapter` with `StripeAdapter` without making changes to the `PaymentProcessor`:
69 |
70 | ```ts
71 | const payment = new PaymentProcessor(new StripeAdapter());
72 | payment.pay(100);
73 | ```
74 |
75 | ### Benefits of the Adapter Pattern
76 |
77 | - **Flexibility**: The Adapter Pattern allows us to switch between different payment providers without altering the core business logic.
78 | - **Reusability**: By creating adapters for each payment provider, we can easily reuse these classes in other parts of the system.
79 | - **Decoupling**: The pattern decouples the client from specific implementations, allowing us to change or add new payment providers with minimal effort.
80 |
81 | The Adapter Pattern is particularly useful when integrating external systems or working with legacy code, as it enables us to standardize interactions without changing the underlying systems.
82 |
83 | ### Conclusion
84 |
85 | In this section, we demonstrated how to use the Adapter Pattern in TypeScript to unify different payment providers under a common interface. This pattern provides flexibility and maintainability, especially when working with multiple services that have different APIs.
86 |
87 | ## Type Inference Problem in Adapter Pattern
88 |
89 | One common issue when using the Adapter Pattern in TypeScript is related to type inference, especially when trying to access specific properties of an adapter. Let’s look at an example that highlights this problem.
90 |
91 | Consider the following code:
92 |
93 | ```ts
94 | const paymentMethod = paymentProcessor.payment.paymentMethod;
95 |
96 | ```
97 |
98 | In this case, `paymentMethod` is inferred to be of type `string`. However, since the `paymentMethod` in our adapters is either `'PayPal'` or `'Stripe'`, we expect the type system to narrow down the type to those specific values. Instead, it defaults to the broader `string` type.
99 |
100 | ### The Problem with Generic `string` Type
101 |
102 | Here’s the full code where this problem occurs:
103 |
104 | ```ts twoslash
105 | export interface PaymentAdapterBase {
106 | paymentMethod: string;
107 | pay(amount: number): void;
108 | }
109 |
110 | export class PayPalAdapter implements PaymentAdapterBase {
111 | paymentMethod = 'PayPal';
112 |
113 | pay(amount: number): void {
114 | console.log(`Pay amount ${amount}$ with PayPal`);
115 | }
116 | }
117 |
118 | export class StripeAdapter implements PaymentAdapterBase {
119 | paymentMethod = 'Stripe';
120 |
121 | pay(amount: number): void {
122 | console.log(`Pay amount ${amount}$ with Stripe`);
123 | }
124 | }
125 |
126 | class PaymentProcessor {
127 | constructor(public readonly payment: PaymentAdapterBase) { }
128 |
129 | pay(amount: number) {
130 | this.payment.pay(amount);
131 | }
132 | }
133 |
134 | const paymentProcessor = new PaymentProcessor(new PayPalAdapter());
135 | paymentProcessor.pay(100);
136 |
137 | const paymentMethod = paymentProcessor.payment.paymentMethod;
138 | // ^?
139 |
140 |
141 |
142 | // ^-- TypeScript infers this as a `string` type,
143 | // but it should be 'PayPal' or 'Stripe'
144 |
145 | ```
146 |
147 | In this example, we defined the `paymentMethod` as a `string` in the `PaymentAdapterBase` interface. However, the adapters (`PayPalAdapter` and `StripeAdapter`) assign specific string literals, `'PayPal'` and `'Stripe'`, to the `paymentMethod` property. When accessed via `paymentProcessor.payment.paymentMethod`, TypeScript does not narrow the type and still treats it as a general `string`.
148 |
149 | ### Why This Happens
150 |
151 | The issue arises because the `PaymentAdapterBase` interface declares `paymentMethod` as a `string`, which is a broad type. Even though each specific adapter provides a more precise string literal, TypeScript defaults to the general type defined in the interface.
152 |
153 | ### Solution: Use String Literal Types
154 |
155 | We can solve this by using string literal types in the interface to ensure that the type system recognizes the specific values used by each adapter. Here's how we can modify the code:
156 |
157 | ```ts twoslash
158 | export interface PaymentAdapterBase {
159 | paymentMethod: 'PayPal' | 'Stripe';
160 | pay(amount: number): void;
161 | }
162 |
163 | export class PayPalAdapter implements PaymentAdapterBase {
164 | paymentMethod: 'PayPal' = 'PayPal';
165 |
166 | pay(amount: number): void {
167 | console.log(`Pay amount ${amount}$ with PayPal`);
168 | }
169 | }
170 |
171 | export class StripeAdapter implements PaymentAdapterBase {
172 | paymentMethod: 'Stripe' = 'Stripe';
173 |
174 | pay(amount: number): void {
175 | console.log(`Pay amount ${amount}$ with Stripe`);
176 | }
177 | }
178 |
179 | class PaymentProcessor {
180 | constructor(public readonly payment: PaymentAdapterBase) { }
181 |
182 | pay(amount: number) {
183 | this.payment.pay(amount);
184 | }
185 | }
186 |
187 | const paymentProcessor = new PaymentProcessor(new PayPalAdapter());
188 | paymentProcessor.pay(100);
189 |
190 | const paymentMethod = paymentProcessor.payment.paymentMethod;
191 | // ^?
192 |
193 |
194 |
195 | // ^--- Now TypeScript correctly infers the type as 'PayPal' or 'Stripe'
196 |
197 | ```
198 |
199 | ### How It Works
200 |
201 | By explicitly defining the `paymentMethod` type in the `PaymentAdapterBase` interface as a union of `'PayPal' | 'Stripe'`, TypeScript can now infer the correct type when accessing the `paymentMethod` property. This ensures that the value will be either `'PayPal'` or `'Stripe'` and not a generic `string`.
202 |
203 | ### Benefits
204 |
205 | - **Type Safety**: The solution provides better type safety. If a new payment method is added, TypeScript will catch any missing or incorrect types.
206 | - **Code Readability**: The correct inference of `paymentMethod` makes the code more readable and understandable, reducing the likelihood of errors when dealing with specific payment providers.
207 |
208 | In conclusion, when using the Adapter Pattern in TypeScript, it's essential to ensure that type inference works as expected by explicitly using string literal types when dealing with properties like `paymentMethod`. This avoids the pitfalls of overly generic types and makes your code more robust and maintainable.
209 |
210 |
211 | ## Problem with Union Type in Adapter Pattern
212 |
213 | When using the union type for the `paymentMethod` property, we encounter a new issue. In the previous example:
214 |
215 | ```ts twoslash
216 | export interface PaymentAdapterBase {
217 | paymentMethod: 'PayPal' | 'Stripe';
218 | pay(amount: number): void;
219 | }
220 |
221 | export class PayPalAdapter implements PaymentAdapterBase {
222 | paymentMethod: 'PayPal' = 'PayPal';
223 |
224 | pay(amount: number): void {
225 | console.log(`Pay amount ${amount}$ with PayPal`);
226 | }
227 | }
228 |
229 | export class StripeAdapter implements PaymentAdapterBase {
230 | paymentMethod: 'Stripe' = 'Stripe';
231 |
232 | pay(amount: number): void {
233 | console.log(`Pay amount ${amount}$ with Stripe`);
234 | }
235 | }
236 |
237 | class PaymentProcessor {
238 | constructor(public readonly payment: PaymentAdapterBase) { }
239 |
240 | pay(amount: number) {
241 | this.payment.pay(amount);
242 | }
243 | }
244 | // ---cut---
245 | const paymentProcessor = new PaymentProcessor(new PayPalAdapter());
246 | paymentProcessor.pay(100);
247 |
248 | const paymentMethod = paymentProcessor.payment.paymentMethod;
249 | // ^?
250 |
251 |
252 |
253 | // ^-- This will be of union type "PayPal" | "Stripe"
254 | ```
255 |
256 | The `paymentMethod` is inferred as a union type `'PayPal' | 'Stripe'`. However, we know that the specific adapter assigned to `PaymentProcessor` is `PayPalAdapter`, meaning the `paymentMethod` should only be `'PayPal'`. This mismatch can lead to problems in terms of accuracy and code clarity.
257 |
258 | ### Union Type Issue
259 |
260 | Even though we are passing a `PayPalAdapter`, TypeScript still treats the `paymentMethod` as a union of both `'PayPal'` and `'Stripe'` due to the union type defined in the `PaymentAdapterBase` interface. This can be confusing because, at runtime, we know that the value will only be `'PayPal'` in this specific instance. This issue arises because the type system doesn’t narrow down the type based on the concrete adapter used.
261 |
262 | ### Scalability Limitation
263 |
264 | Another limitation of the current approach is that `PaymentAdapterBase` only supports two adapters: `PayPalAdapter` and `StripeAdapter`. As your application grows, this becomes less scalable. Adding a new payment method would require extending the union type, which is not future-proof and could lead to maintenance difficulties.
265 |
266 | ### Solution: Generic `PaymentAdapterBase`
267 |
268 | To address both of these issues, we can use generics in the `PaymentAdapterBase` interface. This approach ensures that the `paymentMethod` type is correctly inferred based on the specific adapter used and allows for better scalability as new adapters are added.
269 |
270 | ### Refined Code with Generic Types
271 |
272 | Here is the solution that addresses these issues:
273 |
274 | ```ts twoslash
275 | export interface PaymentAdapterBase {
276 | paymentMethod: TPaymentMethod;
277 | pay(amount: number): void;
278 | }
279 |
280 | export class PayPalAdapter implements PaymentAdapterBase<'PayPal'> {
281 | readonly paymentMethod = 'PayPal';
282 |
283 | pay(amount: number): void {
284 | console.log(`Pay amount ${amount}$ with PayPal`);
285 | }
286 | }
287 |
288 | export class StripeAdapter implements PaymentAdapterBase<'Stripe'> {
289 | readonly paymentMethod = 'Stripe';
290 |
291 | pay(amount: number): void {
292 | console.log(`Pay amount ${amount}$ with Stripe`);
293 | }
294 | }
295 |
296 | class PaymentProcessor {
297 | constructor(public readonly payment: PaymentAdapterBase) { }
298 |
299 | pay(amount: number) {
300 | this.payment.pay(amount);
301 | }
302 | }
303 |
304 | const paymentProcessor = new PaymentProcessor(new PayPalAdapter());
305 | paymentProcessor.pay(100);
306 |
307 | const paymentMethod = paymentProcessor.payment.paymentMethod;
308 | // ^?
309 |
310 |
311 |
312 | // ^-- Now, the paymentMethod is correctly inferred as "PayPal"
313 |
314 | ```
315 |
316 | ### How It Solves the Problem
317 |
318 | 1. **Accurate Type Inference**:
319 | By using the generic `PaymentAdapterBase`, we allow TypeScript to correctly infer the type of `paymentMethod` based on the specific adapter used. In the example above, since we passed `PayPalAdapter` to the `PaymentProcessor`, the `paymentMethod` is inferred as `'PayPal'` and not as a union type.
320 | 2. **Scalability**:
321 | This approach makes the solution more scalable. By using generics, we no longer need to explicitly modify the `PaymentAdapterBase` interface whenever we add a new payment method. Instead, each adapter defines its own specific `paymentMethod`, making it easy to add new adapters in the future without altering the core logic.
322 |
323 | ### Benefits of This Approach
324 |
325 | - **Type-Safe**: The type of `paymentMethod` is now tightly coupled with the specific adapter used, eliminating the ambiguity of the union type.
326 | - **Scalability**: New payment methods can be easily added without modifying the base interface. Each adapter simply defines its own `paymentMethod` type, making the system more maintainable and flexible.
327 | - **Code Clarity**: The code is now easier to read and reason about, as the types are precisely tied to the specific implementations of each adapter.
328 |
329 | ### Conclusion
330 |
331 | The solution presented here resolves the issues of type inference and scalability by using generics in the `PaymentAdapterBase` interface. This ensures that the `paymentMethod` type is accurately inferred based on the adapter used, while also allowing for the seamless addition of new adapters in the future without modifying existing code. This approach not only improves type safety but also enhances the flexibility and maintainability of your application’s architecture.
332 |
333 |
334 | ## Adding a Default Adapter with Type-Safety
335 |
336 | In some cases, it may be useful to define a default adapter in the `PaymentProcessor` class, while still maintaining strict type safety. The goal is to allow the `PaymentProcessor` to default to an adapter (e.g., `PayPalAdapter`) if none is provided, but still ensure that the type of `paymentMethod` remains accurate and specific based on the actual adapter used.
337 |
338 | ### Initial Attempt with Default Adapter
339 |
340 | Let's first attempt to modify the `PaymentProcessor` class to accept an optional adapter and default to `PayPalAdapter` if none is provided:
341 |
342 | ```ts twoslash
343 | export interface PaymentAdapterBase {
344 | paymentMethod: TPaymentMethod;
345 | pay(amount: number): void;
346 | }
347 |
348 | export class PayPalAdapter implements PaymentAdapterBase<'PayPal'> {
349 | readonly paymentMethod = 'PayPal';
350 |
351 | pay(amount: number): void {
352 | console.log(`Pay amount ${amount}$ with PayPal`);
353 | }
354 | }
355 |
356 | export class StripeAdapter implements PaymentAdapterBase<'Stripe'> {
357 | readonly paymentMethod = 'Stripe';
358 |
359 | pay(amount: number): void {
360 | console.log(`Pay amount ${amount}$ with Stripe`);
361 | }
362 | }
363 | // ---cut---
364 | class PaymentProcessor> {
365 | public payment: TAdapter;
366 | constructor(payment?: TAdapter) {
367 | this.payment = payment ?? new PayPalAdapter() as TAdapter;
368 | }
369 |
370 | pay(amount: number) {
371 | this.payment.pay(amount);
372 | }
373 | }
374 |
375 | const paymentProcessor = new PaymentProcessor();
376 | paymentProcessor.pay(100);
377 |
378 | const paymentMethod = paymentProcessor.payment.paymentMethod;
379 | // ^?
380 |
381 |
382 |
383 | // ^-- TypeScript will infer `paymentMethod` as `string`,
384 | // causing the same problem again.
385 |
386 | ```
387 |
388 | While this works in terms of functionality, we face the same problem with type inference. Even though we default to `PayPalAdapter`, the `paymentMethod` is still inferred as `string`, which is not ideal. We want `paymentMethod` to be specific, such as `'PayPal'`, when `PayPalAdapter` is used.
389 |
390 | ### Second Attempt: Incorrect Generic Definition
391 |
392 | After realizing the problem with the union type in the initial attempt, we tried to refactor the code to maintain type safety and allow for a default adapter. However, the following refactor introduces a new issue with how the generic types are defined:
393 |
394 | ```ts twoslash
395 | export interface PaymentAdapterBase {
396 | paymentMethod: TPaymentMethod;
397 | pay(amount: number): void;
398 | }
399 |
400 | export class PayPalAdapter implements PaymentAdapterBase<'PayPal'> {
401 | readonly paymentMethod = 'PayPal';
402 |
403 | pay(amount: number): void {
404 | console.log(`Pay amount ${amount}$ with PayPal`);
405 | }
406 | }
407 |
408 | export class StripeAdapter implements PaymentAdapterBase<'Stripe'> {
409 | readonly paymentMethod = 'Stripe';
410 |
411 | pay(amount: number): void {
412 | console.log(`Pay amount ${amount}$ with Stripe`);
413 | }
414 | }
415 |
416 | class PaymentProcessor> {
417 | constructor(public payment: TAdapter) {}
418 |
419 | pay(amount: number) {
420 | this.payment.pay(amount);
421 | }
422 | }
423 | // ---cut---
424 | const createPaymentProcessor = >(defaultAdapter: TAdapter) => ({
425 | create(adapter?: TAdapter): PaymentProcessor {
426 | if(adapter){
427 | return new PaymentProcessor(adapter) as unknown as PaymentProcessor;
428 | }
429 | return new PaymentProcessor(defaultAdapter) as unknown as PaymentProcessor;
430 | }
431 | });
432 | ```
433 |
434 | In this refactor, we attempt to use a factory function that takes a `defaultAdapter` of type `TAdapter`. The `create` method either accepts a custom adapter or falls back to the `defaultAdapter`.
435 |
436 | Here is how we tried to use it:
437 |
438 | ```ts
439 | const initPaymentProcessor = createPaymentProcessor(new PayPalAdapter());
440 | const paymentProcessor = initPaymentProcessor.create(new StripeAdapter());
441 | ```
442 |
443 | However, this code leads to a **type error**:
444 |
445 | ```
446 | Argument of type 'StripeAdapter' is not assignable to parameter of type 'PayPalAdapter'.
447 | Types of property 'paymentMethod' are incompatible.
448 | Type '"Stripe"' is not assignable to type '"PayPal"'.ts(2345)
449 | ```
450 |
451 | #### Why This Fails
452 |
453 | The issue arises because the generic type `TAdapter` is defined once in the factory function, and the system expects that type to remain consistent throughout the `create` method. In this case, `TAdapter` is inferred as `PayPalAdapter` from the default adapter. When we try to pass a `StripeAdapter`, TypeScript expects the adapter to be compatible with `PayPalAdapter`, which it is not, since `paymentMethod` has different values.
454 |
455 | Essentially, TypeScript expects the `adapter` passed to the `create` method to match the type of the default adapter (`PayPalAdapter`), leading to the type error when trying to pass in `StripeAdapter`.
456 |
457 | #### Key Takeaway
458 |
459 | This second attempt demonstrates the challenge of defining a generic type too strictly. The type system locks in `TAdapter` based on the default adapter, preventing flexibility in passing other adapters later on. This approach fails because it doesn't allow for a different adapter type to be passed without violating the type constraints.
460 |
461 | ### Refactoring for Better Type Inference
462 |
463 | To address this issue, we need to refactor how the default adapter is handled. Specifically, we can introduce a factory function to create a `PaymentProcessor` with a default adapter, while still preserving type safety and allowing for the adapter to be configurable.
464 |
465 | Here’s how we can achieve this:
466 |
467 | ```ts twoslash
468 | export interface PaymentAdapterBase {
469 | paymentMethod: TPaymentMethod;
470 | pay(amount: number): void;
471 | }
472 |
473 | export class PayPalAdapter implements PaymentAdapterBase<'PayPal'> {
474 | readonly paymentMethod = 'PayPal';
475 |
476 | pay(amount: number): void {
477 | console.log(`Pay amount ${amount}$ with PayPal`);
478 | }
479 | }
480 |
481 | export class StripeAdapter implements PaymentAdapterBase<'Stripe'> {
482 | readonly paymentMethod = 'Stripe';
483 |
484 | pay(amount: number): void {
485 | console.log(`Pay amount ${amount}$ with Stripe`);
486 | }
487 | }
488 |
489 | class PaymentProcessor> {
490 | constructor(public payment: TAdapter) {}
491 |
492 | pay(amount: number) {
493 | this.payment.pay(amount);
494 | }
495 | }
496 | // ---cut---
497 | const createPaymentProcessor = >(defaultAdapter: TDefaultAdapter) => ({
498 | create = TDefaultAdapter>(adapter?: TAdapter): PaymentProcessor {
499 | if (adapter) {
500 | return new PaymentProcessor(adapter) as unknown as PaymentProcessor;
501 | }
502 | return new PaymentProcessor(defaultAdapter) as unknown as PaymentProcessor;
503 | }
504 | });
505 |
506 | ```
507 |
508 | In this refactor, the `createPaymentProcessor` function returns an object with a `create` method. The `create` method allows for an optional `adapter` to be passed in, defaulting to the `TDefaultAdapter` type if none is provided. This ensures that the type of `paymentMethod` remains specific to the adapter used, whether it’s the default or a provided one.
509 |
510 | ### Full Example
511 |
512 | ```ts twoslash
513 | export interface PaymentAdapterBase {
514 | paymentMethod: TPaymentMethod;
515 | pay(amount: number): void;
516 | }
517 |
518 | export class PayPalAdapter implements PaymentAdapterBase<'PayPal'> {
519 | readonly paymentMethod = 'PayPal';
520 |
521 | pay(amount: number): void {
522 | console.log(`Pay amount ${amount}$ with PayPal`);
523 | }
524 | }
525 |
526 | export class StripeAdapter implements PaymentAdapterBase<'Stripe'> {
527 | readonly paymentMethod = 'Stripe';
528 |
529 | pay(amount: number): void {
530 | console.log(`Pay amount ${amount}$ with Stripe`);
531 | }
532 | }
533 |
534 | class PaymentProcessor> {
535 | constructor(public readonly payment: TAdapter) { }
536 |
537 | pay(amount: number) {
538 | this.payment.pay(amount);
539 | }
540 | }
541 |
542 | const createPaymentProcessor = >(defaultAdapter: TDefaultAdapter) => ({
543 | create = TDefaultAdapter>(adapter?: TAdapter): PaymentProcessor {
544 | if (adapter) {
545 | return new PaymentProcessor(adapter) as unknown as PaymentProcessor;
546 | }
547 | return new PaymentProcessor(defaultAdapter) as unknown as PaymentProcessor;
548 | }
549 | });
550 |
551 | const initPaymentProcessor = createPaymentProcessor(new PayPalAdapter());
552 | const paymentProcessor = initPaymentProcessor.create(new StripeAdapter());
553 |
554 | paymentProcessor.pay(100);
555 |
556 | const paymentMethod = paymentProcessor.payment.paymentMethod;
557 | // ^?
558 |
559 |
560 |
561 | // ^-- Now, `paymentMethod` is correctly inferred as 'Stripe'.
562 |
563 | ```
564 |
565 | ### How This Works
566 |
567 | 1. **Generics for Flexibility**:
568 | The `createPaymentProcessor` function uses generics to allow the type of the default adapter to be passed in. The `create` method also uses generics to accept either a specific adapter or default to the one provided during initialization.
569 | 2. **Type-Safe Inference**:
570 | By using generics and default types, TypeScript is able to correctly infer the type of `paymentMethod` based on the specific adapter used. If no adapter is passed to `create`, the `paymentMethod` will be inferred based on the default adapter. If an adapter is passed, TypeScript will infer the type based on that adapter.
571 | 3. **Scalability**:
572 | This solution remains scalable. You can easily add more payment adapters without modifying the core logic, and the type system will continue to work correctly, inferring the specific type of `paymentMethod` based on the adapter passed.
573 |
574 | ### Conclusion
575 |
576 | By refactoring the `PaymentProcessor` class to use a factory function and generics, we are able to achieve both type safety and flexibility. This approach allows for a default adapter to be provided while maintaining correct type inference, ensuring that the `paymentMethod` is always accurately typed based on the adapter used.
577 |
578 |
579 |
--------------------------------------------------------------------------------
/docs/design-patterns/builder-pattern.md:
--------------------------------------------------------------------------------
1 | # Builder Pattern
2 |
3 | The builder pattern is a creational design pattern that allows constructing complex objects step by step. The pattern is useful when the construction of an object is complex and requires multiple steps. The builder pattern is used to construct a complex object from simple objects step by step.
4 |
5 |
6 | ```ts twoslash
7 | class Inventory = {}> {
8 |
9 | items: Items = {} as Items;
10 |
11 | add>(value: NewItem) {
12 | this.items = {
13 | ...this.items,
14 | ...value as unknown as Items
15 | }
16 | return this as Inventory;
17 | }
18 | }
19 |
20 | const inventory = new Inventory()
21 | .add({
22 | hello: 'world',
23 | }).add({
24 | typescript: 5.1,
25 | numbers: [23, '123']
26 | });
27 |
28 | console.log(inventory.items.typescript)
29 |
30 |
31 | type A = typeof inventory.items;
32 | // ^?
33 |
34 |
35 |
36 |
37 |
38 |
39 | // End of the example
40 | ```
41 |
42 |
43 | ## Examples
44 |
45 | List of Sidebar Items in Vitepress is not a type-safe, but we can use Record Object to make it type-safe.
46 |
47 | ```ts twoslash
48 | import type { DefaultTheme } from "vitepress";
49 |
50 | export type SidebarMetadata = Omit & {
51 | order?: number;
52 | };
53 | export type AbsoluteLink = string;
54 | export type RelativeLink = string;
55 |
56 | export class Sidebar<
57 | Groups extends Record = {},
58 | Items extends Record = {}
59 | > {
60 | items: Items = {} as Items;
61 | groups: Groups = {} as Groups;
62 |
63 | constructor(group?: Groups, items?: Items) {
64 | this.groups = group ?? ({} as Groups);
65 | this.items = items ?? ({} as Items);
66 | }
67 |
68 | addGroup(group: Link, value: SidebarMetadata) {
69 | this.groups = {
70 | ...this.groups,
71 | ...(value as unknown as Groups),
72 | };
73 | return this as unknown as Sidebar, Items>;
74 | }
75 |
76 | add(group: keyof Groups, key: Link, value: SidebarMetadata) {
77 | this.items = {
78 | ...this.items,
79 | ...(value as unknown as Items),
80 | };
81 | return this as unknown as Sidebar>;
82 | }
83 |
84 | toSidebarItems(): DefaultTheme.SidebarItem[] {
85 | return Object.entries(this.items).map(([link, metadata]) => ({
86 | ...metadata,
87 | link,
88 | }));
89 | }
90 |
91 | override(group: keyof Groups, key: keyof Items, value: SidebarMetadata) {
92 | this.items = {
93 | ...this.items,
94 | ...(value as unknown as Items),
95 | };
96 | return this as unknown as Sidebar;
97 | }
98 |
99 | clone(): Sidebar {
100 | return new Sidebar(this.groups, this.items);
101 | }
102 | }
103 |
104 | const baseSidebar = new Sidebar()
105 | .addGroup("/", { text: "Start Reading", collapsed: true })
106 | .addGroup("/type-programming", { text: "Type Programming", collapsed: true })
107 | .addGroup("/type-programming/loop", { text: "Loop", collapsed: true })
108 | .add("/type-programming/loop", "intro", { text: "Introduction" });
109 |
110 | const thSidebar = baseSidebar.clone();
111 |
112 | thSidebar.override("/type-programming/loop", "intro", { text: "Mapped Types" });
113 | ```
114 |
115 | In this example, we define a `Sidebar` class that uses a record object to manage sidebar items in Vitepress. The `Sidebar` class has two generic parameters: `Groups` and `Items`. The `Groups` parameter is a record object with keys of type `AbsoluteLink` and values of type `SidebarMetadata`. The `Items` parameter is a record object with keys of type `RelativeLink` and values of type `SidebarMetadata`.
116 |
117 | The `Sidebar` class has methods to add groups and items to the sidebar, override existing items, and clone the sidebar. The `toSidebarItems` method converts the sidebar items to an array of `DefaultTheme.SidebarItem` objects.
118 |
119 | We create a base sidebar with three groups and one item. We then clone the base sidebar and override the item in the cloned sidebar. This demonstrates how we can use a record object to manage sidebar items in a type-safe manner.
120 |
121 | ## Example Projects
122 | - Hono
123 | - Elysia
124 | - Zod
--------------------------------------------------------------------------------
/docs/design-patterns/design-patterns.md:
--------------------------------------------------------------------------------
1 | # Design Patterns
2 |
3 | In order to write type-safe and maintainable code in TypeScript, it is important to follow best practices and design patterns. Design patterns are reusable solutions to common problems that arise when writing software. They provide a way to structure code in a way that is easy to understand, maintain, and extend.
4 |
5 | Not all design pattern meet type-safety, but some of them are very useful in TypeScript. Here are some design patterns that are commonly used in TypeScript:
6 |
7 | - [Use builder pattern](./builder-pattern.md) when constructing complex objects step by step, passing type information along the way.
8 | - [Use function argument instead of plain object](./function-argument.md) to enforce type safety and avoid runtime errors.
9 | - [Use function overload](./function-overload.md) to create multiple functions with the same name but different parameters, achieving polymorphism in programming languages that do not support it natively.
--------------------------------------------------------------------------------
/docs/design-patterns/function-argument.md:
--------------------------------------------------------------------------------
1 | ---
2 | outline: deep
3 | ---
4 |
5 | # Function Argument
6 |
7 | A function argument is a value passed to a function when it is called. Arguments are used to provide input to the function so that it can perform its task. Functions can have zero or more arguments, and the number of arguments a function can accept is determined by its signature.
8 |
9 | Let's see step by step how to implement the Function Argument pattern.
10 |
11 | ## Example 1: Fetcher
12 |
13 | In this example, we simply want to fetch data from a URL using the `fetch` API.
14 |
15 | ```ts twoslash
16 | const result = await fetch("http://localhost:3000/api/users", {
17 | method: "GET",
18 | });
19 | ```
20 |
21 | ### Wrapping with a Function
22 | When we want to make it more reusable, we can create a function that accepts the URL and the method as arguments.
23 |
24 | ```ts twoslash
25 | // Define the fetcher options
26 | interface FetcherOptions {
27 | baseUrl: string;
28 | path?: string;
29 | method?: "GET" | "POST" | "PUT" | "DELETE";
30 | }
31 |
32 | // Define the fetcher function
33 | function fetcher(options: FetcherOptions) {
34 | return fetch(`${options.baseUrl}${options.path}`, {
35 | method: options.method ?? "GET",
36 | });
37 | }
38 |
39 | // Usage
40 | await fetcher({ baseUrl: "http://localhost:3000", path: "/api/users" });
41 | ```
42 |
43 | In this example, we created a `fetcher` function that accepts an object with the `baseUrl`, `path`, and `method` properties. This function is more reusable and can be used to fetch data from different URLs.
44 |
45 | ### Using a Function Argument
46 |
47 | However, when we want to call `fetcher`, we need to passing the `baseUrl` in every call. To make it more convenient, we can create a function that returns the `fetcher` function result with the `baseUrl` already set.
48 |
49 | ```ts{3,9-30} twoslash
50 | ///
51 | // Define the fetcher options
52 | type FetcherOptionFunc = (context: Context) => {
53 | path?: string;
54 | method?: "GET" | "POST" | "PUT" | "DELETE";
55 | logMessage?: string;
56 | };
57 |
58 | type Env = "dev" | "prod";
59 | type EnvOptions = {
60 | activeEnv: Env;
61 | env: Record<
62 | Env,
63 | {
64 | baseUrl: string;
65 | }
66 | >;
67 | };
68 |
69 | const context: EnvOptions = {
70 | activeEnv: "dev",
71 | env: {
72 | dev: {
73 | baseUrl: "http://localhost:3000",
74 | },
75 | prod: {
76 | baseUrl: "https://api.example.com",
77 | },
78 | },
79 | };
80 |
81 | function fetcher(optionFunction: FetcherOptionFunc) {
82 | const options = optionFunction(context); // [!code highlight]
83 |
84 | if (options.logMessage) console.log(options.logMessage); // [!code highlight]
85 |
86 | const baseUrl = context.env[context.activeEnv].baseUrl; // [!code highlight]
87 | return fetch(`${baseUrl}${options.path}`, {
88 | method: options.method ?? "GET",
89 | });
90 | }
91 |
92 | await fetcher((c) => ({
93 | path: "/api/users",
94 | logMessage: // [!code highlight]
95 | `[Env=${c.activeEnv}] Fetching users from ${c.env[c.activeEnv].baseUrl}`, // [!code highlight]
96 | }));
97 | ```
98 |
99 | In this example, we created a `fetcher` function that accepts a function that returns an object with the `baseUrl`, `path`, `method`, and `logMessage` properties. This function is more flexible and allows us to set the `baseUrl` based on the environment. We can also add a `logMessage` property to log a message before fetching the data.
100 |
101 | ### Decoupling the Function Logic
102 |
103 | By using the Function Argument pattern, we can create more flexible and reusable functions that accept different arguments and configurations. This pattern allows us to decouple the function logic from the input values, making the function more versatile and easier to maintain.
104 |
105 | ```ts{10-13} twoslash
106 | ///
107 | /**
108 | * Define the fetcher options
109 | */
110 | type FetcherOptionFunc = (context: Context) => {
111 | path?: string;
112 | method?: "GET" | "POST" | "PUT" | "DELETE";
113 | logMessage?: string;
114 | };
115 | type EnvBase = {
116 | activeEnv: string,
117 | env: Record,
118 | }
119 |
120 | /**
121 | * Create a fetcher function with the context
122 | */
123 | function createFetcher(context: Context) { // [!code highlight]
124 | return function fetcher(optionFunction: FetcherOptionFunc) {
125 | const options = optionFunction(context);
126 |
127 | if (options.logMessage) console.log(options.logMessage);
128 |
129 | const baseUrl = context.env[context.activeEnv].baseUrl;
130 | return fetch(`${baseUrl}${options.path}`, {
131 | method: options.method ?? "GET",
132 | });
133 | };
134 | } // [!code highlight]
135 |
136 | /**
137 | * Usage
138 | */
139 |
140 | type Env = "dev" | "prod";
141 | type EnvOptions = {
142 | activeEnv: Env;
143 | env: Record<
144 | Env,
145 | {
146 | baseUrl: string;
147 | }
148 | >;
149 | };
150 |
151 | const fetcher = createFetcher({ // [!code highlight]
152 | activeEnv: process.env.NODE_ENV === "production" ? "prod" : "dev",
153 | env: {
154 | dev: {
155 | baseUrl: "http://localhost:3000",
156 | },
157 | prod: {
158 | baseUrl: "https://api.example.com",
159 | },
160 | },
161 | }); // [!code highlight]
162 |
163 | await fetcher((c) => ({
164 | path: "/api/users",
165 | logMessage:
166 | `[Env=${c.activeEnv}] Fetching users from ${c.env[c.activeEnv].baseUrl}`,
167 | }));
168 | ```
169 |
170 | In this example, we created a `createFetcher` function that returns a `fetcher` function with the context already set. This allows us to create a `fetcher` function with the environment configuration and reuse it multiple times without passing the context every time.
171 |
172 | ## Real World Examples
173 |
174 | ### Example 1: OData Query Builder
175 |
176 | [OData](https://www.odata.org/) is a standard protocol for querying and updating data. It defines a set of query options that clients can use to request data from a server. The OData query builder is a library that helps you build OData queries in a type-safe way.
177 |
178 | ```ts twoslash
179 | import { ODataExpression} from 'ts-odata-client';
180 |
181 | interface User {
182 | firstName: string;
183 | lastName: string;
184 | }
185 |
186 | const expression = ODataExpression
187 | .forV4()
188 | .filter((p) =>
189 | p.firstName.$equals("john")
190 | );
191 |
192 | console.log(`Query String: ${expression.build().filter}`);
193 | // Output: Query String: $filter=firstName eq 'john'
194 | ```
195 |
196 | For this example, we are using the `ts-odata-client` library to build an OData query. The `ODataExpression` class provides a fluent API for building OData queries. In this example, we are filtering the users whose `firstName` is equal to "john".
197 |
198 | ### How Function Argument Works
199 |
200 | Focus on the `filter` function argument, you can see that the `filter` function accepts a predicate function that takes a parameter `p`. The `p` parameter is an object that represents the properties of the `User` interface. You can access the properties of the `User` interface, which is `firstName` and `lastName`, and use them to build the filter expression.
201 |
202 | Without the Function Argument pattern, you would have to pass the `User` object directly to the `filter` function, which would make the code less readable and harder to maintain.
203 |
204 | ```ts twoslash
205 | import { ODataExpression} from 'ts-odata-client';
206 |
207 | interface User {
208 | firstName: string;
209 | lastName: string;
210 | }
211 |
212 | const expression = ODataExpression
213 | .forV4()
214 | .filter((p) => { // [!code focus]
215 | type FilterFunction = typeof p; // [!code focus]
216 | // ^?
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 | return p.firstName.$equals("john") // [!code focus]
231 | });
232 | ```
233 |
234 | ### ODataExpression Implementation
235 |
236 | From source code [ts-odata-client@2.0.2](https://github.com/cbrianball/ts-odata-client/blob/7b55184beebe5a08437863035f7bac29c341025a/src/lib/ODataQueryBase.ts#L106-L119), you can see how the `filter` function is implemented using the Function Argument pattern.
237 |
238 | ```ts
239 | // File: src/lib/ODataQueryBase.ts
240 |
241 | export class ODataQueryBase> {
242 |
243 | // ...
244 | public filter(
245 | predicate: // [!code focus]
246 | | BooleanPredicateBuilder // [!code focus]
247 | | ((builder: EntityProxy, functions: FilterAccessoryFunctions) => BooleanPredicateBuilder), // [!code focus]
248 | ) {
249 | if (typeof predicate === "function")
250 | predicate = predicate(
251 | this.provider[createProxiedEntity]() as unknown as EntityProxy,
252 | new FilterAccessoryFunctions(),
253 | );
254 |
255 | const expression = new Expression(ExpressionOperator.Predicate, [predicate], this.expression);
256 | return this.provider.createQuery(expression);
257 | }
258 |
259 | // ...
260 | }
261 | ```
262 |
263 |
264 |
265 |
266 | ## Example Projects
267 | - [ts-odata-client](https://github.com/cbrianball/ts-odata-client) - A TypeScript library for building OData queries.
268 |
269 |
270 |
271 |
--------------------------------------------------------------------------------
/docs/design-patterns/function-overload.md:
--------------------------------------------------------------------------------
1 | # Function Overloading
2 |
3 | Function overloading is a feature that allows creating multiple functions with the same name but different parameters. It is a way to achieve polymorphism in programming languages that do not support it natively.
4 |
5 | ## Prerequisites
6 |
7 | - [TypeScript Function Overloading](https://dmitripavlutin.com/typescript-function-overloading/)
8 |
9 | ## Examples
10 |
11 | ```ts
12 | import { ODataQueryProvider } from 'ts-odata-client';
13 |
14 | export class ODataV4QueryProvider extends ODataQueryProvider {
15 | executeQueryAsync(expression?: Expression): Promise;
16 | executeQueryAsync(odataUrl: string): Promise;
17 | async executeQueryAsync(value?: Expression | string): Promise {
18 | if (typeof value !== "string") value = this.buildQuery(value);
19 | const response = await this.sendRequest(value);
20 |
21 | if (response.ok) {
22 | return (await response.json()) as T;
23 | }
24 |
25 | throw new Error(JSON.stringify(await response.json()));
26 | }
27 | }
28 | ```
29 |
30 | From the example above, the `executeQueryAsync` method is overloaded to accept either an `Expression` or a `string` parameter. This allows the method to be called with different types of arguments while maintaining the same name. From [ts-odata-client](https://github.com/cbrianball/ts-odata-client/blob/7b55184beebe5a08437863035f7bac29c341025a/src/lib/ODataV4QueryProvider.ts#L35-L46)
31 |
32 | ## Example Projects
33 | - Hono
34 | - ts-odata-client
--------------------------------------------------------------------------------
/docs/design-patterns/loosen-and-tighten.md:
--------------------------------------------------------------------------------
1 | # Loosen and Tighten
2 |
3 | Simple design patterns to help trick the TypeScript compiler into doing what you want.
4 |
5 | For example, you can use the `as` keyword to loosen the type of a variable, and then use the `as` keyword again to tighten the type of the variable.
6 |
7 | ```ts twoslash
8 | const value = 'hello' as unknown as number;
9 | // ^?
10 | ```
--------------------------------------------------------------------------------
/docs/examples.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | Reading open source projects is a great way to learn how to write better code. However, it can be difficult to understand the codebase of a large project. In this book, I will provide examples of code snippets from well-known open-source projects such as Zod, tRPC, Hono, Elysia, and ts-odata-client. I will explain the code and show you how to apply the same patterns to your own projects.
4 |
5 | Most Type-safe in the Modern TypeScript libraries use Template Literal Types, you can checkout [TypeScript Awesome Template Literal Types](https://github.com/ghoullier/awesome-template-literal-types) which is a curated list of awesome things related to TypeScript Template Literal Types.
6 |
7 | ## Modern TypeScript Libraries
8 |
9 | - Zod
10 | - tRPC
11 | - Hono
12 | - Elysia
13 | - ts-odata-client
14 | - [hotscript](https://github.com/gvergnaud/hotscript) - A library of composable functions for the type-level! Transform your TypeScript types in any way you want using functions you already know.
15 | - [zodios](https://www.zodios.org/)
16 | - [query-key-factory](https://github.com/lukemorales/query-key-factory) A library for creating typesafe standardized query keys, useful for cache management in @tanstack/query
17 | - [Effect](https://github.com/Effect-TS/effect) An ecosystem of tools to build robust applications in TypeScript.
--------------------------------------------------------------------------------
/docs/framework-pattern/config-file.md:
--------------------------------------------------------------------------------
1 | # Config File
2 |
3 | When we talk about config file, it look like it's simpest feature while writing the code, however,
4 | behind the scene, they need to be designed with complex logic and type interface to ensure that the config file is type-safe.
5 |
6 | ## Using Simple Object
7 |
8 | For example:
9 |
10 | In next.js, they use `next.config.mjs` to configure the next.js project.
11 |
12 | ```ts
13 | // @ts-check
14 |
15 | /**
16 | * @type {import('next').NextConfig}
17 | */
18 | const nextConfig = {
19 | /* config options here */
20 | }
21 |
22 | export default nextConfig
23 | ```
24 |
25 | ## Using Function as a Config
26 |
27 | For example:
28 |
29 | In Vite or Vitest, they use `defineConfig` to configure the project.
30 |
31 | ```ts
32 | import { defineConfig } from 'vite'
33 |
34 | export default defineConfig({
35 | /* config options here */
36 | })
37 | ```
--------------------------------------------------------------------------------
/docs/framework-pattern/framework-pattern.md:
--------------------------------------------------------------------------------
1 | # Framework Pattern
2 |
3 | Many Frameworks provide better type-safe support for TypeScript, this section will provide technique and design pattern that most framework apply type-safe.
4 |
5 |
--------------------------------------------------------------------------------
/docs/framework-pattern/generate-dynamic-types.md:
--------------------------------------------------------------------------------
1 | # Generate Dynamic Types
2 |
3 | [astro v4.x](https://github.com/withastro/astro),
4 |
5 | [contentlayer v0.3.x](https://github.com/contentlayerdev/contentlayer)
--------------------------------------------------------------------------------
/docs/glossary.md:
--------------------------------------------------------------------------------
1 | ---
2 | outline: deep
3 | ---
4 |
5 | # Glossary
6 |
7 | ## T
8 |
9 | ### Type loosing
10 |
11 | **Type loosing**: The process of increase possibility of types or broadening types when converting from one type to another.
12 |
13 | For example, when you convert from `'UP'` to `'UP' | 'DOWN'`, you are broadening the type. That's means you are loosing type information. because the possibility of types is increased, not only `'UP'` but also `'DOWN'`.
14 |
15 | ### Type tightening
16 |
17 | **Type tightening**: The process of decrease possibility of types or narrowing types when converting from one type to another.
18 |
19 | For example, when you convert from `'UP' | 'DOWN'` to `'UP'`, you are narrowing the type. That's means you are tightening type information. because the possibility of types is decreased, not only `'UP' | 'DOWN'` but also `'UP'`.
20 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: "Type-safe Design Pattern"
7 | text: "in Modern TypeScript"
8 | tagline: This book provide ready to use design pattern for type-safe approach in modern typescript
by Thada Wangthammang
9 | actions:
10 | - theme: brand
11 | text: Starting Reading
12 | link: /intro
13 | - theme: alt
14 | text: What is Type-safe?
15 | link: /what-type-safe
16 | - theme: alt
17 | text: อ่านภาษาไทย 🚧
18 | link: /th/intro
19 |
20 | features:
21 | - title: 🎉 Next Level TypeScript
22 | details: Level up your TypeScript skills with advanced design patterns and best practices
23 | - title: 💪 Data Structure
24 | details: Not all data structure are type-safe, this book provide ready to use data structure for type-safe approach
25 | - title: 🎨 Design Patterns
26 | details: Not all design pattern meet type-safety, this book provide ready to use design pattern for type-safe approach
27 | ---
28 |
29 |
--------------------------------------------------------------------------------
/docs/intro.md:
--------------------------------------------------------------------------------
1 | ---
2 | outline: deep
3 | ---
4 |
5 |
6 | # Introduction
7 |
8 | ::: warning
9 | This book is still in the draft phase. Please feel free to open an issue or submit a pull request on [GitHub](https://github.com/mildronize/type-safe-design-pattern)
10 | :::
11 |
12 | ::: info
13 | This book is more advanced than the TypeScript Handbook. It is intended for developers who are already familiar with TypeScript and are looking to improve their code quality and design.
14 | :::
15 |
16 | This book is a collection of TypeScript best practices and design patterns. It is intended to be a guide for developers who are looking to improve their TypeScript code quality and design. The book covers a wide range of topics, including TypeScript configuration, data structures, design patterns, and more.
17 |
18 | After examining the code of several well-known open-source projects such as Zod, tRPC, Hono, Elysia, and ts-odata-client.
19 |
20 | ## Author Talk
21 |
22 | I am currently writing a new E-book. Please follow and support!This book is for advanced TypeScript users (not suitable for beginners, it might be overwhelming). The title of the book is **"Type-safe Design Pattern in Modern TypeScript."** Not every Design Pattern can be made Type-safe, but I have compiled frequently used Design Patterns in Modern TypeScript Libraries.
23 |
24 | Moreover, I have not seen any books or articles that cover these topics. I hope this book will help you improve your TypeScript code quality and design. I will publish the book on Leanpub. I will update the progress on X (Twitter). Please follow me on X (Twitter) [@mildronize](https://x.com/mildronize). I've been working on this book for a while, and I hope to finish it soon. I would love to hear feedback from the community because at that time when I'm writing, I haven't seen anyone write about this yet.
25 |
26 | ## Why I wrote this book
27 |
28 | I'm not sure if everyone is like me, but when I started writing TypeScript, generics were really scary (not generics in other languages, but specifically TypeScript's).
29 |
30 | The more I wrote in a type-safe manner, the more it felt like going into an unknown territory that left me confused, haha.
31 |
32 | And of course, the concept of type-safe isn't new, and I didn't come up with it. Many modern TypeScript libraries have started to claim that they are type-safe libraries. You can check them out.
33 |
34 | There are also articles (not many, but there are some) about programming with types, or what you might call type-level programming or a complete Turing system.
35 |
36 | However, after reading those articles, I still couldn't design my own framework. It requires imagination and involves looking into well-known open-source code and gradually testing the concept of type programming to see how types work together.
37 |
38 | One thing I've discovered is that when I write JavaScript code first (without types), it can't be transformed into type-safe code without rewriting. Therefore, older libraries that support TypeScript, like Express, can never be truly type-safe.
39 |
40 | So, when you see people claiming that modern TypeScript is type-safe, it means TypeScript first, then JavaScript. In other words, you write the types before writing the code. I'm trying to understand how to shift my thinking and design my code to be type-safe, which requires a change in mindset.
41 |
42 | As I wrote in my book:
43 |
44 | > "Not every design pattern has type-safe data types. Applying the appropriate design patterns is essential."
45 |
46 | I've learned type-safe design patterns from various modern TypeScript open-source projects, and write my own Azure Functions Framework called [Nammtham](https://nammatham.thaitype.dev/). I've learned a lot from these projects.
47 |
48 | I decided to write this book to help other developers who are struggling with type-safe design patterns. I hope this book will help you improve your TypeScript code quality and design.
49 |
50 | ## Prerequisites
51 | Please make sure you have a good understanding of TypeScript before reading this book. If you are new to TypeScript, I recommend reading the [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) first.
52 |
53 | Generics are a fundamental concept in TypeScript, so make sure you understand them. You can read the [Generics](https://www.typescriptlang.org/docs/handbook/2/generics.html) section of the TypeScript Handbook.
54 |
55 | ## How the book is structured
56 |
57 | From the book prerequisites, it is recommended to have a good understanding of TypeScript before reading this book. However, if you are already familiar with TypeScript, you can jump to a specific section that interests you. Each section will provide the necessary prerequisites and type knowledge that you need to know before reading it. So, you don't necessarily have to read the book from start to finish.
58 |
59 | The book is divided into several sections, each covering a different topic. Each section is linked together, the sections are as follows:
60 |
61 | - Section 1: Start Reading
62 | - Section 2: Design Guideline
63 | - Section 3: Basic Types
64 | - Section 4: Type Programming
65 | - Section 5: Design Patterns
66 | - Section 6: Framework Patterns
67 | - Section 7: Performance
68 |
69 | ## Reading Instructions
70 |
71 | This book contains many code examples, with [twoslash plugin](https://shiki.matsu.io/packages/vitepress#twoslash) you can peak the type definitions by hovering over the code. For example:
72 |
73 | ```ts twoslash
74 | type User = {
75 | name: string
76 | age: number
77 | }
78 |
79 | type UserWithoutAge = Omit
80 | ```
81 |
82 | I recommend that you try running the code examples in the [TypeScript Playground](https://www.typescriptlang.org/play) or your own editor to get a better understanding of the concepts. However, I you need interactive examples I don't have enough time to support it, feel free to open the Pull Request on [GitHub](https://github.com/mildronize/type-safe-design-pattern) for [adding monao editor](https://github.com/vuejs/vitepress/issues/1508#issuecomment-1689500884) to the page.
83 |
84 | ## Recommended Reading
85 |
86 | - [Type-Level Programming](https://type-level-typescript.com/) e-book.
87 | There are many free chapters available. However, I recommend buying the book to support the author. See more in [YouTube](https://www.youtube.com/watch?v=vGVvJuazs84) and his [slide](https://docs.google.com/presentation/d/18Y0M4SRjKoJGR3ePSBBn8yPlpkE5biufZRdHo1Ka2AI/edit?usp=sharin).
88 | - [Effective TypeScript: 62 Specific Ways to Improve Your TypeScript](https://learning.oreilly.com/library/view/effective-typescript/9781098155056/) By Dan Vanderkam
89 | - [TypeScript Cookbook](https://learning.oreilly.com/library/view/typescript-cookbook/9781098136642/) By Stefan Baumgartner
90 |
91 | ## Disclaimer
92 |
93 | I did not create these design patterns. I learned from various modern TypeScript open-source projects, including famous ones like Zod, tRPC, Hono, Elysia, and many others, as well as writing the [Nammtham](https://nammatham.thaitype.dev/) (Azure Functions Framework). This process has helped me distill commonly used patterns.
94 |
95 | The type-safe design patterns is not suitable for every project. It is intended for projects that require high code quality and design. Make sure to evaluate whether these patterns are suitable for your project before using them.
96 |
97 | ## Contributing
98 | If you have any suggestions or feedback, please feel free to open an issue or submit a pull request on [GitHub](https://github.com/mildronize/type-safe-design-pattern)
99 |
100 | You can both contibute to the content and the translation:
101 |
102 | - **Content Contribution**, you can contribute by adding new design patterns, best practices, or improving the existing content, this is allowed only through the English version.
103 | - **Translation Contribution**, you can contribute by translating the content to your language. I've recommened to use the prompt from the generative AI like GPT-3, GPT-4, etc. to generate the content for the page. You can see the example in the [Prompt](./prompt.md) page.
104 | - For consistency, please do not translate the content directly, but use the prompt to generate the content. When result of the prompt is not good, you update your prompt condition and try again, you can see the example in the [Thai Prompt](/th/prompt.md) page.
105 | - When you copy the output from the ChatGPT, I recommend to paste at any WSYSIWYG editor like [Typora](https://typora.io/) or Notion to see the markdown preview, then you can copy the markdown content and paste it to the page.
--------------------------------------------------------------------------------
/docs/performance/performance.md:
--------------------------------------------------------------------------------
1 | # Performance
2 |
3 | ::: info
4 | This topic topic is quite new some developer discssion about TypeScript Performance and how to improve it.
5 | :::
6 |
7 | Are you looking to optimize your TypeScript code for better performance? This talk will guide you through identifying and resolving performance issues in TypeScript. She cover effective debugging techniques, leveraging the TypeScript compiler to spot potential problems, and using profiling tools to trace and fix bottlenecks. By mastering these methods, you'll be able to enhance the efficiency and maintainability of your TypeScript projects.
8 |
9 |
10 | [](https://www.youtube.com/watch?v=lJ63-j0OHG0)
11 |
12 | [Aleksandra Sikora](https://x.com/aleksandrasays) - Typescript Performance: Going Beyond The Surface, here is video summary:
13 |
14 | **Breaking Down Large Type Helpers**
15 |
16 | TypeScript's type helpers are powerful tools for defining complex types, but when they become too large, they can negatively impact performance. By breaking a large type helper, like a 60-line `ComputeThing`, into smaller, more focused type helpers, you allow TypeScript to cache these smaller pieces more effectively. This practice not only improves performance but also makes the codebase easier to maintain and understand. Smaller type helpers are more modular and can be reused in different parts of the application.
17 |
18 | **Using `tsc --generateTrace` for Performance Tracing**
19 |
20 | TypeScript provides a built-in tool for generating performance traces, which can be extremely useful for identifying performance bottlenecks in your TypeScript code. The command `tsc --generateTrace outDir` generates a set of JSON files in the specified output directory (`outDir`). These JSON files can be imported into Chrome Dev Tools, which provides a visual representation of the performance trace. In Chrome Dev Tools, you can analyze the traces to see which parts of your TypeScript code are taking the most time to compile. This detailed information helps in pinpointing specific files or types that are causing performance issues, allowing you to optimize them for better performance.
21 |
22 | By breaking down type helpers and using performance tracing tools, you can significantly improve the efficiency and maintainability of your TypeScript codebase.
23 |
24 | ## TypeScript Performance Walkthrough
25 |
26 |
27 | [](https://www.youtube.com/watch?v=ZL3z1oBZntk)
28 |
29 | You can watch the full talk on [TypeScript Performance with Aleksandra Sikora](https://www.youtube.com/watch?v=ZL3z1oBZntk) by Matt Pocock and Aleksandra Sikora
--------------------------------------------------------------------------------
/docs/performance/typescript-performance-aleksandra.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mildronize/type-safe-design-pattern/886a4ad916f84d60af2e88be68fb0acea8486465/docs/performance/typescript-performance-aleksandra.png
--------------------------------------------------------------------------------
/docs/performance/typescript-performance-going-beyond-the-surface.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mildronize/type-safe-design-pattern/886a4ad916f84d60af2e88be68fb0acea8486465/docs/performance/typescript-performance-going-beyond-the-surface.png
--------------------------------------------------------------------------------
/docs/prompt.md:
--------------------------------------------------------------------------------
1 | # Prompt
2 |
3 | This page is special page that collect the prompt from generative AI for example GPT-3, GPT-4, etc. The prompt is used to generate the content for the page.
4 |
5 | ## Localization Prompt List
6 | - [Thai Prompt](./th/prompt.md)
--------------------------------------------------------------------------------
/docs/public/staticwebapp.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingSlash": "never",
3 | "responseOverrides": {
4 | "404": {
5 | "rewrite": "/404.html"
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/docs/th/examples.md:
--------------------------------------------------------------------------------
1 | # ตัวอย่าง
2 |
3 | การอ่านโครงการโอเพนซอร์สเป็นวิธีที่ดีในการเรียนรู้วิธีการเขียนโค้ดให้ดีขึ้น อย่างไรก็ตาม การเข้าใจโค้ดของโครงการขนาดใหญ่อาจเป็นเรื่องยาก ในหนังสือเล่มนี้ ผมจะยกตัวอย่างโค้ดจากโครงการโอเพนซอร์สที่เป็นที่รู้จัก เช่น Zod, tRPC, Hono, Elysia, และ ts-odata-client ผมจะอธิบายโค้ดและแสดงให้คุณเห็นวิธีการใช้แนวทางการออกแบบ (Design Pattern) เดียวกันในโครงการของคุณเอง
4 |
5 | ส่วนใหญ่การออกแบบชนิดข้อมูลแบบปลอดภัย (Type-safe Design Patterns) ในไลบรารี TypeScript สมัยใหม่ (Modern TypeScript Libraries) ใช้ Template Literal Types คุณสามารถดู [TypeScript Awesome Template Literal Types](https://github.com/ghoullier/awesome-template-literal-types) ซึ่งเป็นรายการที่รวบรวมสิ่งที่ยอดเยี่ยมที่เกี่ยวข้องกับ TypeScript Template Literal Types
6 |
7 | ## ไลบรารี TypeScript สมัยใหม่
8 |
9 | - Zod
10 | - tRPC
11 | - Hono
12 | - Elysia
13 | - ts-odata-client
14 | - [hotscript](https://github.com/gvergnaud/hotscript) - ไลบรารีของฟังก์ชันที่ประกอบกันได้สำหรับระดับชนิดข้อมูล! แปลงชนิดข้อมูล TypeScript ของคุณในแบบที่คุณต้องการโดยใช้ฟังก์ชันที่คุณรู้จักอยู่แล้ว
15 | - [zodios](https://www.zodios.org/)
--------------------------------------------------------------------------------
/docs/th/glossary.md:
--------------------------------------------------------------------------------
1 | ---
2 | outline: deep
3 | ---
4 |
5 | # คลังคำศัพท์
6 |
7 | ## T
8 |
9 | ### Type loosing
10 |
11 | **การทำให้ชนิดข้อมูลหลวมลง** (Type loosing): กระบวนการที่เพิ่มความเป็นไปได้ของชนิดข้อมูลหรือการขยายชนิดข้อมูลเมื่อแปลงจากชนิดข้อมูลหนึ่งไปยังอีกชนิดข้อมูลหนึ่ง
12 |
13 | ตัวอย่างเช่น เมื่อคุณแปลงจาก `'UP'` เป็น `'UP' | 'DOWN'` คุณกำลังขยายชนิดข้อมูล นั่นหมายความว่าคุณกำลังทำให้ข้อมูลชนิดข้อมูลหลวมลงเพราะความเป็นไปได้ของชนิดข้อมูลเพิ่มขึ้น ไม่เพียงแค่ `'UP'` แต่ยังมี `'DOWN'` ด้วย
14 |
15 | ### Type tightening
16 |
17 | **การทำให้ชนิดข้อมูลเข้มข้นขึ้น** (Type tightening): กระบวนการที่ลดความเป็นไปได้ของชนิดข้อมูลหรือการจำกัดชนิดข้อมูลเมื่อแปลงจากชนิดข้อมูลหนึ่งไปยังอีกชนิดข้อมูลหนึ่ง
18 |
19 | ตัวอย่างเช่น เมื่อคุณแปลงจาก `'UP' | 'DOWN'` เป็น `'UP'` คุณกำลังจำกัดชนิดข้อมูล นั่นหมายความว่าคุณกำลังทำให้ข้อมูลชนิดข้อมูลเข้มข้นขึ้นเพราะความเป็นไปได้ของชนิดข้อมูลลดลง จาก `'UP' | 'DOWN'` เป็นเพียง `'UP'`
--------------------------------------------------------------------------------
/docs/th/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: "การออกแบบชนิดข้อมูลแบบปลอดภัย"
7 | text: "ใน TypeScript สมัยใหม่"
8 | tagline: หนังสือเล่มนี้รวบรวมรูปแบบการออกแบบ (Design Pattern) ที่เขียนด้วย TypeScript โดยชนิดข้อมูลมีความปลอดภัย (Type-safe) และเข้าใจง่าย
โดยธาดา หวังธรรมมั่ง
9 | actions:
10 | - theme: brand
11 | text: เริ่มอ่าน
12 | link: /th/intro
13 | - theme: alt
14 | text: ความหมายของ Type-safe?
15 | link: /th/what-type-safe
16 |
17 | features:
18 | - title: 🎉 ยกระดับทักษะ TypeScript
19 | details: ยกระดับทักษะ TypeScript ของคุณด้วยรูปแบบการออกแบบขั้นสูงและแนวปฏิบัติที่ดีนำไปใช้งานได้จริง
20 | - title: 💪 โครงสร้างข้อมูล
21 | details: ไม่ใช่ทุกโครงสร้างข้อมูลที่ชนิดข้อมูลมีความปลอดภัย (Type-safe) หนังสือเล่มนี้มีรวบรวมโครงสร้างข้อมูลพร้อมใช้งานสำหรับแนวทางที่ปลอดภัย
22 | - title: 🎨 รูปแบบการออกแบบ
23 | details: ไม่ใช่ทุกรูปแบบการออกแบบที่ที่ชนิดข้อมูลมีความปลอดภัย (Type-safe) การประยุกต์ใช้รูปแบบการออกแบบที่เหมาะสมเป็นสิ่งสำคัญ
24 | ---
25 |
26 |
--------------------------------------------------------------------------------
/docs/th/intro.md:
--------------------------------------------------------------------------------
1 | ---
2 | outline: deep
3 | ---
4 |
5 | # บทนำ
6 |
7 | ::: warning คำเตือน
8 | ตอนนี้ยังไม่ได้เขียนแปลไทยไว้ แต่สามารถอ่านแบบภาษาอังกฤษได้ก่อน และรอให้เขียนเสร็จแล้วจะมาแปลเป็นภาษาไทยต่อ และบางหน้าทดลองแปลด้วย AI ไว้บ้าง ถ้าคำที่เขียนไม่สละสลวย สามารถช่วยแก้ไขได้
9 |
10 | โดยที่ภาษาอังกฤษจะเป็น Single Source of Truth, และจะไม่มีการใส่เนื้อหาเพิ่มในส่วนของภาษาไทย แต่จะเป็นการแปลมาเท่านั้น โดยภาษาไทยผมได้เตรียม Template ไว้แล้วสามารถ copy เนื้อหาภาษาอังกฤษ และสามารถเปิด Issue หรือส่ง Pull Request ได้ที่ [GitHub](https://github.com/mildronize/type-safe-design-pattern)
11 | :::
12 |
13 | ::: info ข้อมูล
14 | หนังสือเล่มนี้มีความซับซ้อนมากกว่าคู่มือ TypeScript ซึ่งเหมาะสำหรับนักพัฒนาที่มีความรู้ใน TypeScript อยู่แล้ว และต้องการปรับปรุงคุณภาพและการออกแบบโค้ดของตัวเอง
15 | :::
16 |
17 | หนังสือเล่มนี้เป็นการรวบรวมแนวปฏิบัติที่ดีนำไปใช้งานได้จริงและแนวทางการออกแบบใน TypeScript ซึ่งตั้งใจให้เป็นคู่มือสำหรับนักพัฒนาที่ต้องการปรับปรุงคุณภาพและการออกแบบโค้ด TypeScript หนังสือครอบคลุมหัวข้อต่าง ๆ อย่างกว้างขวาง รวมถึงการตั้งค่า TypeScript, โครงสร้างข้อมูล, แนวทางการออกแบบ และอื่น ๆ
18 |
19 | หลังจากที่ได้ตรวจสอบโค้ดของโครงการโอเพนซอร์สที่มีชื่อเสียงหลายโครงการ เช่น Zod, tRPC, Hono, Elysia, และ ts-odata-client
20 |
21 | ## พูดคุยกับผู้เขียน
22 |
23 | ผมกำลังเขียน E-book เล่มใหม่ โปรดติดตามและสนับสนุน! หนังสือเล่มนี้สำหรับผู้ใช้ TypeScript ขั้นสูง (ไม่เหมาะสำหรับผู้เริ่มต้น อาจจะซับซ้อนเกินไป) ชื่อของหนังสือคือ **"การออกแบบชนิดข้อมูลแบบปลอดภัยใน TypeScript สมัยใหม่"** ไม่ใช่ทุกแนวทางการออกแบบสามารถทำให้ชนิดข้อมูลแบบปลอดภัยได้ แต่ผมได้รวบรวมแนวทางการออกแบบที่ใช้งานบ่อยในห้องสมุด TypeScript สมัยใหม่
24 |
25 | นอกจากนี้ผมยังไม่เคยเห็นหนังสือหรือบทความที่ครอบคลุมหัวข้อเหล่านี้ ผมหวังว่าหนังสือเล่มนี้จะช่วยปรับปรุงคุณภาพและการออกแบบโค้ด TypeScript ของคุณ ผมจะเผยแพร่หนังสือบน Leanpub และจะอัปเดตความคืบหน้าบน X (Twitter) โปรดติดตามผมบน X (Twitter) [@mildronize](https://x.com/mildronize) ผมทำงานกับหนังสือเล่มนี้มาระยะหนึ่งแล้วและหวังว่าจะเสร็จในเร็ว ๆ นี้ ผมอยากได้ยินความคิดเห็นจากชุมชนเพราะตอนที่ผมเขียนยังไม่เห็นมีใครเขียนเกี่ยวกับเรื่องนี้เลย
26 |
27 | ## สารบัญ
28 |
29 | - การตั้งค่า TypeScript
30 | - ใช้ strict
31 | - โครงสร้างข้อมูล
32 | - ใช้ [literal type](/basic-types/literal-types) แทน string
33 | - ใช้ [Record object](/basic-types/record-object.md) แทน list/array
34 | - ใช้ [Tuple](/basic-types/tuple.md) แทน list/array
35 | - รูปแบบการออกแบบ
36 | - ใช้ [builder pattern](/design-patterns/builder-pattern.md)
37 | - ใช้ [function argument](/design-patterns/function-argument.md) แทน plain object
38 | - ใช้ [function overload](/design-patterns/function-overload.md)
39 |
40 | ## สิ่งที่ควรเรียนรู้มาก่อน
41 |
42 | โปรดแน่ใจว่าคุณมีความเข้าใจที่ดีเกี่ยวกับ TypeScript ก่อนอ่านหนังสือเล่มนี้ หากคุณยังใหม่กับ TypeScript ผมแนะนำให้อ่าน [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) ก่อน
43 |
44 | Generics เป็นแนวคิดพื้นฐานใน TypeScript ดังนั้นโปรดแน่ใจว่าคุณเข้าใจ คุณสามารถอ่านส่วนของ [Generics](https://www.typescriptlang.org/docs/handbook/2/generics.html) ใน TypeScript Handbook
45 |
46 | ## หนังสือและเอกสารที่แนะนำให้อ่าน
47 |
48 | - [Type-Level Programming](https://type-level-typescript.com/) e-book
49 | มีหลายบทที่เปิดให้อ่านฟรี อย่างไรก็ตามผมแนะนำให้ซื้อหนังสือเพื่อสนับสนุนผู้เขียน ดูเพิ่มเติมใน [YouTube](https://www.youtube.com/watch?v=vGVvJuazs84) และ [slide](https://docs.google.com/presentation/d/18Y0M4SRjKoJGR3ePSBBn8yPlpkE5biufZRdHo1Ka2AI/edit?usp=sharin) ของเขา
50 | - [Effective TypeScript: 62 Specific Ways to Improve Your TypeScript](https://learning.oreilly.com/library/view/effective-typescript/9781098155056/) โดย Dan Vanderkam
51 | - [TypeScript Cookbook](https://learning.oreilly.com/library/view/typescript-cookbook/9781098136642/) โดย Stefan Baumgartner
52 |
53 | ## คำชี้แจงที่สำคัญ
54 |
55 | ผมไม่ได้สร้างแนวทางการออกแบบเหล่านี้ ผมเรียนรู้จากโครงการโอเพนซอร์ส TypeScript สมัยใหม่หลายโครงการ รวมถึงโครงการที่มีชื่อเสียงเช่น Zod, tRPC, Hono, Elysia, และอื่น ๆ รวมถึงการเขียน [Nammtham](https://nammatham.thaitype.dev/) (Azure Functions Framework) กระบวนการนี้ช่วยให้ผมกลั่นกรองแนวทางการออกแบบที่ใช้บ่อย
56 |
57 | การออกแบบชนิดข้อมูลแบบปลอดภัยไม่เหมาะกับทุกโครงการ มันเหมาะกับโครงการที่ต้องการคุณภาพและการออกแบบโค้ดที่สูง โปรดประเมินว่าแนวทางการออกแบบเหล่านี้เหมาะสมกับโครงการของคุณหรือไม่ก่อนนำไปใช้
58 |
59 | ## การมีส่วนร่วม (Contributing)
60 |
61 | ถ้าคุณมีข้อเสนอแนะหรือความคิดเห็น โปรดเปิด Issue หรือส่ง Pull Request บน [GitHub](https://github.com/mildronize/type-safe-design-pattern) ได้ตามสบาย
62 |
63 | คุณสามารถมีส่วนร่วมทั้งในด้านเนื้อหาและการแปล:
64 |
65 | - **การมีส่วนร่วมในเนื้อหา** (Content Contribution) คุณสามารถมีส่วนร่วมโดยการเพิ่มแนวทางการออกแบบ (Design Patterns) ใหม่ ๆ แนวปฏิบัติที่ดีนำไปใช้งานได้จริง (Best Practices) หรือปรับปรุงเนื้อหาที่มีอยู่ ซึ่งทำได้เฉพาะในเวอร์ชันภาษาอังกฤษเท่านั้น
66 | - **การมีส่วนร่วมในการแปล** (Translation Contribution) คุณสามารถมีส่วนร่วมโดยการแปลเนื้อหาเป็นภาษาของคุณ ผมแนะนำให้ใช้การสร้างเนื้อหาจาก AI เช่น GPT-3, GPT-4 เป็นต้น เพื่อสร้างเนื้อหาสำหรับหน้า คุณสามารถดูตัวอย่างได้ในหน้าของ [Prompt](./prompt.md)
67 | - เพื่อความสม่ำเสมอ โปรดอย่าแปลเนื้อหาโดยตรง แต่ใช้ prompt เพื่อสร้างเนื้อหา เมื่อผลลัพธ์ของ prompt ไม่ดี คุณสามารถปรับปรุงเงื่อนไขของ prompt และลองใหม่ได้ คุณสามารถดูตัวอย่างได้ในหน้าของ [Thai Prompt](/th/prompt.md)
68 | - เมื่อคุณคัดลอกผลลัพธ์จาก ChatGPT ผมแนะนำให้วางในโปรแกรม WSYSIWYG editor อย่าง [Typora](https://typora.io/) หรือ Notion เพื่อดูตัวอย่างการแสดงผล markdown จากนั้นคุณสามารถคัดลอกเนื้อหา markdown และวางลงในหน้าได้
--------------------------------------------------------------------------------
/docs/th/project-structure.md:
--------------------------------------------------------------------------------
1 | # การตั้งค่าและโครงสร้างโปรเจกต์
2 |
3 | ## การตั้งค่าคอมไพลเลอร์ TypeScript
4 |
5 | ไฟล์การตั้งค่าคอมไพลเลอร์ TypeScript คือ `tsconfig.json` ไฟล์นี้ใช้เพื่อกำหนดค่าคอมไพลเลอร์ TypeScript คอมไพลเลอร์ TypeScript จะใช้ไฟล์นี้เพื่อตัดสินใจว่าจะคอมไพล์โปรเจกต์อย่างไร ใช้โหมด `strict` เพื่อเปิดใช้ตัวเลือกการตรวจสอบประเภทอย่างเข้มงวดทั้งหมด ซึ่งจะช่วยให้คุณเจอข้อผิดพลาดได้มากขึ้นในระหว่างการคอมไพล์
6 |
7 |
8 | ```json
9 | {
10 | "compilerOptions": {
11 | "strict": true
12 | }
13 | }
14 | ```
15 |
16 | ## โครงสร้างโปรเจกต์
17 |
18 | ::: warning คำชี้แจงที่สำคัญ
19 | การจัดระเบียบโครงสร้างโปรเจกต์และชนิดข้อมูลอาจแตกต่างกันไปขึ้นอยู่กับขนาดและความซับซ้อนของโปรเจกต์ จากประสบการณ์ของผม การออกแบบโครงสร้างโปรเจกต์ต่อไปนี้ใช้ได้ดีกับโปรเจกต์ส่วนใหญ่ อย่างไรก็ตาม ความต้องการและบริบทเฉพาะของโปรเจกต์อาจจำเป็นต้องมีการปรับเปลี่ยนจากแนวปฏิบัติเหล่านี้ ความยืดหยุ่นและการคิดวิเคราะห์เป็นกุญแจสำคัญในการนำหลักการเหล่านี้ไปใช้อย่างมีประสิทธิภาพ
20 | :::
21 |
22 | ### การจัดระเบียบโมดูล (Module Organization)
23 |
24 | **แนวทางโมดูล** (Modular Approach): ผมมักจัดระเบียบโค้ดและชนิดข้อมูลในลักษณะโมดูล ซึ่งหมายถึงการสร้างโมดูลย่อย ๆ ที่มีความเป็นอิสระแทนที่จะมีทุกอย่างในไฟล์เดียว ฟังก์ชันหรือคลาสที่ทำงานร่วมกันบ่อย ๆ จะถูกจัดให้อยู่ในโมดูลเดียวกัน
25 |
26 | แนวทางนี้สอดคล้องกับหลักการความรับผิดชอบเดียว (Single Responsibility Principle - SRP) จากหลักการ SOLID ที่ส่งเสริมให้นักพัฒนาจัดโครงสร้างโค้ดให้แต่ละโมดูลหรือไฟล์มีความรับผิดชอบเพียงอย่างเดียว
27 |
28 | ### การจัดระเบียบการประกาศชนิดข้อมูล (Type Definition Organization)
29 |
30 | **ชนิดข้อมูลแบบใกล้เคียง** (Inline Types): ชนิดข้อมูลที่เฉพาะเจาะจงกับโมดูล (เช่น พารามิเตอร์สำหรับฟังก์ชันหรือคลาส) จะถูกกำหนดภายในโมดูลนั้น ๆ วิธีนี้ช่วยให้ชนิดข้อมูลที่เกี่ยวข้องอยู่ใกล้กับโค้ดที่ใช้งาน ทำให้โค้ดอ่านง่ายและเข้าใจง่ายขึ้น ซึ่งมีประโยชน์โดยเฉพาะสำหรับชนิดข้อมูลที่เฉพาะเจาะจงกับโมดูลหนึ่ง ๆ และไม่ได้ใช้ซ้ำที่อื่น
31 |
32 | **ชนิดข้อมูลที่ใช้ร่วมกัน** (Shared Types): สำหรับชนิดข้อมูลที่ต้องใช้ร่วมกันระหว่างหลายคลาส ผมจะสร้างไฟล์แยกต่างหาก โดยปกติจะใช้ชื่อว่า `types.ts` ภายในโมดูลเดียวกัน ไฟล์นี้จะประกอบด้วยอินเตอร์เฟสหรือชนิดข้อมูลที่ใช้ร่วมกันซึ่งเฉพาะเจาะจงกับโมดูลนั้น ๆ (Domain อย่างเคร่งครัด) วิธีนี้ช่วยป้องกันไม่ให้การประกาศชนิดข้อมูลรกในไฟล์โมดูลหลักและช่วยให้ชนิดข้อมูลที่เกี่ยวข้องถูกจัดระเบียบ
33 |
34 | **ชนิดข้อมูลทั่วไป/ใช้ร่วมกันทั้งโปรเจกต์** (Common/Global Types): นอกจากนี้ยังมีชนิดข้อมูลทั่วไปที่ใช้ทั่วทุกโมดูล เช่น ชนิดข้อมูลที่ไม่เฉพาะเจาะจงกับ business logic หรือโมดูลใด ๆ ชนิดข้อมูลเหล่านี้จะถูกวางไว้ในไดเรกทอรีทั่วไป ทำให้สามารถเข้าถึงได้จากส่วนใดส่วนหนึ่งของโปรเจกต์ วิธีนี้ช่วยให้ชนิดข้อมูลทั่วไปเข้าถึงได้ง่ายและไม่จำเป็นต้องทำซ้ำ
35 |
36 | ### ไฟล์ประกาศชนิดข้อมูล (Type Declaration Files)
37 |
38 | ผมแนะนำให้หลีกเลี่ยงการใช้ไฟล์ `.d.ts` เว้นแต่จำเป็นจริง ๆ ควรใช้ไฟล์เหล่านี้อย่างระมัดระวัง เนื่องจากการประกาศสำหรับใช้ร่วมกันทั้งโปรเจกต์ (global declarations) อาจจัดการและบำรุงรักษาได้ยากเมื่อโปรเจกต์ขยายตัว นอกจากนี้ ยังอาจทำให้เกิดความไม่ตรงกันระหว่างชนิดข้อมูลในระหว่างการคอมไพล์และการรันไทม์ ทำให้เกิดความซับซ้อนมากขึ้น ควรประกาศชนิดข้อมูลภายในโมดูลและใช้เส้นทางการนำเข้าหรือการนำเข้าแบบ wildcard (`import * as types from ...`) เพื่อจัดการการนำเข้าชนิดข้อมูลอย่างมีประสิทธิภาพ
39 |
40 | อย่างไรก็ตาม ไฟล์ `.d.ts` สามารถมีประโยชน์มากในการประกาศชนิดข้อมูลสำหรับใช้ร่วมกันทั้งโปรเจกต์ (global types) โดยเฉพาะเมื่อทำงานกับไลบรารีที่ติดตั้งจากภายนอก (third-party libraries) หรือเมื่อขยายชนิดข้อมูลที่มีมาให้ใช้งานเริ่มต้น (built-in types) การหลีกเลี่ยงการใช้ไฟล์ `.d.ts` อย่างสมบูรณ์อาจไม่เป็นไปได้ในทุกสถานการณ์ การใช้ไฟล์เหล่านี้อย่างรอบคอบและจัดการอย่างระมัดระวังเพื่อหลีกเลี่ยงปัญหาเป็นสิ่งสำคัญ
41 |
42 | ### สรุป
43 |
44 | หลักการสำหรับการจัดระเบียบชนิดข้อมูลคล้ายกับการจัดระเบียบโค้ด JavaScript: แยกตาม Domain, โมดูล และ business logic หากคุณใช้เฟรมเวิร์กที่รองรับการออกแบบชนิดข้อมูลแบบปลอดภัย (Type-safe Design Patterns) ความจำเป็นในการเขียนการประกาศชนิดข้อมูลอาจลดลงอย่างมาก เนื่องจากเฟรมเวิร์กเหล่านี้มักจะคาดเดาชนิดข้อมูลโดยอัตโนมัติ ในกรณีนี้ คุณเพียงแค่ต้องประกาศ interface ของข้อมูลเพื่อเสริมการคาดเดาชนิดข้อมูลของเฟรมเวิร์ก
45 |
46 | วิธีนี้ช่วยให้โครงสร้างโปรเจกต์สะอาดและดูแลรักษาง่าย ซึ่งสามารถขยายตัวได้ดีเมื่อโปรเจกต์เติบโตขึ้น
--------------------------------------------------------------------------------
/docs/th/prompt.md:
--------------------------------------------------------------------------------
1 | # Thai Prompt
2 |
3 | หน้านี้เป็นหน้าพิเศษที่รวบรวม prompt จาก generative AI เช่น GPT-3, GPT-4 เป็นต้น Prompt เหล่านี้ใช้ในการสร้างเนื้อหา
4 |
5 | ## ChatGPT-4o
6 |
7 | แปลไทยโดยมีเงื่อนไขดังนี้
8 |
9 | - การเลือกใช้คำภาษาไทย
10 | - คำว่า `ฉัน` ให้ใช้คำว่า `ผม` แทน
11 | - คำว่า `ของตน` ให้ใช้คำว่า `ของตัวเอง` แทน
12 | - คำว่า `การอนุมาน` ให้ใช้คำว่า `การคาดเดา` แทน
13 | - การเลือกคำแปลจาก ภาษาอังกฤษเป็นภาษาไทย โดยที่ถ้าพูดถึงคำนั้นครั้งแรกของเอกสารที่แปลทั้งหมด ให้ใส่วงเล็บเพื่อใส่ภาษาอังกฤษที่เป็นคำเดิมไว้ด้วย
14 | - คำว่า `Type-safe` ให้แปลว่า `ชนิดข้อมูลแบบปลอดภัย`
15 | - คำว่า `Best Practice` ให้แปลว่า `แนวปฏิบัติที่ดีนำไปใช้งานได้จริง`
16 | - คำว่า `Design Pattern` ให้แปลว่า `แนวทางการออกแบบ`
17 | - คำว่า `Modular` ให้แปลว่า `โมดูล`
18 | - คำว่า `Inline Types` ให้แปลว่า `ชนิดข้อมูลแบบใกล้เคียง`
19 | - คำว่า `Shared Types` ให้แปลว่า `ชนิดข้อมูลที่ใช้ร่วมกัน`
20 | - คำว่า `Common/Global Types` ให้แปลว่า `ชนิดข้อมูลทั่วไป/ใช้ร่วมกันทั้งโปรเจกต์`
21 | - คำว่า `global declarations` ให้แปลว่า `การประกาศสำหรับใช้ร่วมกันทั้งโปรเจกต์`
22 | - คำว่า `global types` ให้แปลว่า `ชนิดข้อมูลสำหรับใช้ร่วมกันทั้งโปรเจกต์`
23 | - คำว่า `third-party libraries` ให้แปลว่า `ไลบรารีที่ติดตั้งจากภายนอก`
24 | - คำว่า `built-in types` ให้แปลว่า `ชนิดข้อมูลที่มีมาให้ใช้งานเริ่มต้น`
25 | - คำที่ไม่ต้องการให้มีวงเล็บภาษาอังกฤษที่เป็นคำเดิม
26 | - `Example`
27 | - `Error`
28 | - `Contributing`
29 | - `Glossary`
30 | - การเลือกคำแปลจาก ภาษาอังกฤษเป็นภาษาไทย
31 | - คำว่า `Modern` ให้แปลว่า `สมัยใหม่`
32 | - คำว่า `Prerequisites` ให้แปลว่า `สิ่งที่ควรเรียนรู้มาก่อน`
33 | - คำว่า `Recommended Reading` ให้แปลว่า `หนังสือและเอกสารที่แนะนำให้อ่าน`
34 | - คำว่า `Disclaimer` ให้แปลว่า `คำชี้แจงที่สำคัญ`
35 | - คำว่า `open an issue ... on Github` ให้แปลว่า `เปิด Issue ... บน Github`
36 | - คำว่า `Traditional Type` ให้แปลว่า `การประกาศชนิดข้อมูลแบบดั้งเดิม`
37 | - คำว่า `type inference` ให้แปลว่า `การคาดเดาชนิดข้อมูล`
38 | - คำว่า `type system` ให้แปลว่า `ระบบชนิดข้อมูล`
39 | - คำว่า `definition of types` หรือ `type definition` ให้แปลว่า `การประกาศชนิดข้อมูล`
40 | - คำว่า `Let's break down` ให้แปลว่า `ลองพยายามอธิบาย`
41 | - คำว่า `Type loosing` ให้แปลว่า `การทำให้ชนิดข้อมูลหลวมลง`
42 | - คำว่า `Type tightening` ให้แปลว่า `การทำให้ชนิดข้อมูลเข้มข้นขึ้น`
43 | - คำว่า `codebase` ให้แปลว่า `โค้ด`
44 | - คำว่า `TypeScript Libraries` ให้แปลว่า `ไลบรารี TypeScript`
45 | - คำว่า `Glossary` ให้แปลว่า `คลังคำศัพท์`
46 | - คำว่า `Project` ให้แปลว่า `โปรเจกต์`
47 | - คำว่า `catch ... errors` ให้แปลว่า `เจอ ... ข้อผิดพลาด`
48 | - คำว่า `data interfaces` ให้แปลว่า `interface ของข้อมูล`
49 | - การเลือกคำคำที่มีบริบทข้างเคียง ซึ่งแต่ละที่อาจจะแปลไม่เหมือนกัน
50 | - คำว่า `Type` อาจจะแปลว่า `ชนิดข้อมูล` หรือ `ประเภท` ให้พิจารณาจากบริบทข้างเคียง
51 | - คำที่ไม่ต้องการแปล
52 | - `Pull Request`
53 | - `Libraries`
54 | - `E-book`
55 | - `business logic`
56 | - `Domain`
--------------------------------------------------------------------------------
/docs/th/what-type-safe.md:
--------------------------------------------------------------------------------
1 | # การออกแบบชนิดข้อมูลแบบปลอดภัยคืออะไร? (Type-safe Design Patterns)
2 |
3 | ::: tip TL;DR:
4 | **การออกแบบชนิดข้อมูลแบบปลอดภัย** (Type-safe) ช่วยให้แน่ใจว่าประเภทของข้อมูลในโปรแกรมถูกต้องและสอดคล้องกัน โดยเฉพาะในภาษาโปรแกรมสมัยใหม่อย่าง TypeScript ลักษณะสำคัญรวมถึงการใช้ประเภท generic และ [template literal types](/basic-types/template-literal-types), แนวทางการออกแบบแบบ builder (Builder Pattern), การคาดเดาชนิดข้อมูล (Automatic Type Inference), และการลดการซ้ำซ้อนของการประกาศชนิดข้อมูล ตัวอย่างของโครงการ TypeScript ที่ใช้การออกแบบชนิดข้อมูลแบบปลอดภัย ได้แก่ Zod, tRPC, Hono, และ Elysia
5 | :::
6 |
7 | หลายคนในวงการโปรแกรมมิ่งมักพูดถึง **"การออกแบบชนิดข้อมูลแบบปลอดภัย"** (Type-safe) แต่มีน้อยคนที่จะพยายามให้คำนิยามที่ชัดเจน เพื่อความชัดเจนยิ่งขึ้น ลองพยายามอธิบายการออกแบบชนิดข้อมูลแบบปลอดภัยในบริบทของภาษาโปรแกรมสมัยใหม่อย่าง TypeScript ในความเข้าใจของผม
8 |
9 | การออกแบบชนิดข้อมูลแบบปลอดภัย (Type-safe) คือแนวทางการออกแบบที่ช่วยให้แน่ใจว่าประเภทของข้อมูลในโปรแกรมถูกต้องและสอดคล้องกัน แนวทางนี้มีคุณค่าอย่างยิ่งในภาษาโปรแกรมสมัยใหม่อย่าง TypeScript ซึ่งมีระบบชนิดข้อมูล (type system) ที่ทรงพลังเพื่อเพิ่มความถูกต้องและความเชื่อถือได้ของโปรแกรม
10 |
11 | ## การประกาศชนิดข้อมูลแบบดั้งเดิม (Traditional Type)
12 |
13 | เมื่อผมพูดถึงการประกาศชนิดข้อมูลแบบดั้งเดิม (Traditional Type) ผมหมายถึงประเภทที่ถูกกำหนดในโปรแกรม ไม่ใช่แค่ประเภทใน TypeScript แต่ยังรวมถึงประเภทในภาษาโปรแกรมอื่น ๆ เช่น Java, C# เป็นต้น
14 |
15 | การประกาศชนิดข้อมูลแบบดั้งเดิม (Traditional Type) คือวิธีการกำหนดประเภทของข้อมูลในโปรแกรม มันสามารถเป็นประเภท primitive เช่น `number`, `string`, `boolean` เป็นต้น หรือประเภทซับซ้อนเช่น `interface`, `class` เป็นต้น
16 |
17 | ตัวอย่างเช่น ใน TypeScript เราสามารถกำหนดประเภทได้ดังนี้:
18 |
19 | ```ts
20 | interface Person {
21 | name: string;
22 | age: number;
23 | }
24 | ```
25 |
26 | ## ลักษณะของการออกแบบชนิดข้อมูลแบบปลอดภัย (Characteristics of Type-safe)
27 |
28 | มีหลายไลบรารีและเฟรมเวิร์กที่ให้การสนับสนุนการออกแบบชนิดข้อมูลแบบปลอดภัย (Type-safe) นี่คือลักษณะบางอย่างเมื่อพวกเขาอ้างว่าพวกเขาออกแบบชนิดข้อมูลแบบปลอดภัย:
29 |
30 | - **Generic**: พวกเขาใช้ประเภท generic
31 | - **Template Literal Types**: พวกเขาใช้ template literal types
32 | - **Builder Pattern**: พวกเขาใช้แนวทางการออกแบบแบบ builder
33 | - **Automatic Type Inference**: พวกเขาใช้การคาดเดาชนิดข้อมูลอัตโนมัติผ่านฟังก์ชันและประเภท generic
34 | - **Reduce Type Duplicate Definition**: พวกเขาลดการซ้ำซ้อนในการประกาศชนิดข้อมูล
--------------------------------------------------------------------------------
/docs/type-programming/conditional-types.md:
--------------------------------------------------------------------------------
1 | # Conditional Types
2 |
3 | In TypeScript, conditional types are a powerful feature that allows you to create types that depend on a condition. Conditional types are used to create types that change based on the properties of other types. They are often used in generic types to create more flexible and reusable code.
4 |
5 | ```ts twoslash
6 | // Define a type that checks if a type is an array
7 | type IsArray = T extends any[] ? "yes" : "no";
8 |
9 | // Test the IsArray type
10 | type Test1 = IsArray; // "no"
11 | type Test2 = IsArray; // "yes"
12 | ```
13 |
14 | ## Using Infer
15 |
16 | ### Prerequisites
17 | - [Template Literal Types](../basic-types/template-literal-types)
18 |
19 | Infer is a keyword used in conditional types to infer the type of a variable based on a condition. Infer is often used in conjunction with the `extends` keyword to extract the type of a variable that matches a specific condition.
20 |
21 | ```ts twoslash
22 | type FindPrefix =
23 | T extends `${infer Prefix}/${string}`
24 | ? Prefix
25 | : 'Not Found';
26 |
27 |
28 | type Test = FindPrefix<'api/users'>;
29 | // ^?
30 |
31 | // End of Example
32 | ```
33 |
34 | For example above, the `FindPrefix` type extracts the prefix of a string that is separated by a `/`. If the string matches the pattern `${infer Prefix}/${string}`, the type `Prefix` is inferred as the prefix of the string. Otherwise, the type is set to `'Not Found'`.
35 |
--------------------------------------------------------------------------------
/docs/type-programming/examples.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | A curated list of awesome things related to Type Programming
4 |
5 |
6 | ## Extracting URL Parameters
7 |
8 |
9 |
10 | ```ts twoslash
11 | // ✅ Correct type example
12 | navigate("user/:userId", { userId: "2"});
13 |
14 | // ✅ Correct type example, which is `dashboardId` is optional.
15 | navigate("user/:userId/dashboard(/:dashboardId)", { userId: "2" });
16 |
17 | // ❌ Incorrect Type, `userId` is missing. Add one to fix the error!
18 | // @ts-expect-error
19 | navigate("user/:userId/dashboard(/:dashboardId)", { dashboardId: "2" });
20 |
21 | // ❌ Incorrect Type, `oops` isn't a parameter. Remove it to fix the error!
22 | // @ts-expect-error
23 | navigate("user/:userId/dashboard(/:dashboardId)", { userId: "2", oops: ":(" });
24 |
25 | // Here is the implementation
26 |
27 | type ParseUrlParams =
28 | url extends `${infer path}(${infer optionalPath})`
29 | ? ParseUrlParams & Partial>
30 | : url extends `${infer start}/${infer rest}`
31 | ? ParseUrlParams & ParseUrlParams
32 | : url extends `:${infer param}`
33 | ? { [k in param]: string }
34 | : {};
35 |
36 | // navigate to a different route
37 | function navigate(
38 | path: T,
39 | params: ParseUrlParams
40 | ) {
41 | // interpolate params
42 | let url = Object.entries(params).reduce(
43 | (path, [key, value]) => path.replace(`:${key}`, value),
44 | path
45 | );
46 |
47 | // clean url
48 | url = url.replace(/(\(|\)|\/?:[^\/]+)/g, '')
49 |
50 | // update url
51 | history.pushState({}, '', url);
52 | }
53 | ```
54 |
55 | The example from [Type-level Programming](https://type-level-typescript.com/) book. It shows how to extract URL parameters from a string and navigate to a different route.
56 |
57 |
--------------------------------------------------------------------------------
/docs/type-programming/loop/mapped-types.md:
--------------------------------------------------------------------------------
1 | # Mapped Types
2 |
3 | In generally programming, a map is a data structure that allows us to store key-value pairs. In type programming, we can think of a mapped type as a map. We can declare a mapped type that maps a set of keys to a set of values.
4 |
5 | ## Declare a Mapped Type
6 |
7 | For example, we can declare a mapped type `CopyMap` that just copies the input type `T`. This is similar to declaring a map that copies the key-value pairs of another map. The result of the mapped type `CopyMap` is the same as the input type `T`.
8 |
9 | ```ts twoslash
10 | // Declare a mapped type
11 | type Person = {
12 | name: string;
13 | age: number;
14 | };
15 |
16 | type CopyMap = {
17 | [Key in keyof T]: T[Key];
18 | };
19 |
20 | // Test the mapped type
21 | type Test = CopyMap;
22 | // ^?
23 |
24 |
25 |
26 |
27 | // End of the example
28 | ```
29 |
30 | In the example above, `CopyMap` Type return the same thing that input from `T`, we declare a mapped type `CopyMap` that maps a set of keys `Key` to a set of values `T[Key]`. We use the `keyof` operator to get the keys of the type `T` and the `in` operator to iterate over the keys. We then use the key `Key` to access the value `T[Key]` and map it to the key `Key`.
31 |
32 | ## Thinking Mapped Types are Loops
33 |
34 | Too hard to understand? You can think of mapped types as loops. We loop in each key of `Person` and map it to the value of `Person` type. Check the js-like pseudo code below:
35 |
36 | ```js
37 | function CopyMap(Person) {
38 | const Result = {};
39 | for(const Key in Object.keys(Person)) {
40 | Result[Key] = Person[Key];
41 | }
42 | return Result;
43 | }
44 | ```
45 |
46 | But wait!, we can't use `for` loop in type programming. We use mapped types to loop over the keys of a type and map them to the values of the type. This is similar to using a loop to iterate over the keys of an object and map them to the values of the object.
47 |
48 | Go back to `CopyMap` type, in the code below:
49 |
50 | ```ts twoslash
51 | type CopyMap = {
52 | [Key in keyof T]: T[Key];
53 | };
54 | ```
55 |
56 | We use the `keyof` operator which returns the keys of the type `Person`, that is `"name" | "age"` in this case. We then use the `in` operator to iterate over the keys and map them to the values of the type `Person`. The result of the mapped type `CopyMap` is the same as the input type `Person`.
57 |
58 | Note that, when we use `in` operator, we can act of union types to be arrary of keys (From the programming realm, we can't do this). For example, `keyof Person` returns `"name" | "age"`, we can think of it as `["name", "age"]` in the programming realm.
59 |
60 | ## Examples
61 |
62 | ### 1. Loop for Add Prefix to Keys
63 |
64 | ```ts twoslash
65 | type AppendPrefix = {
66 | [K in keyof T as `${U}${K & string}`]: T[K]
67 | };
68 |
69 | function appendPrefix, U extends string>(obj: T, prefix: U) {
70 | const result: Partial> = {};
71 | for (const [key, value] of Object.entries(obj)) {
72 | result[`${prefix}${key}`] = value;
73 | }
74 | return result;
75 | }
76 |
77 | interface Person {
78 | name: string;
79 | age: number;
80 | location: string;
81 | }
82 |
83 | const person: Person = {
84 | name: 'John',
85 | age: 30,
86 | location: 'Thailand'
87 | }
88 |
89 | // Example
90 |
91 | const oldPerson = appendPrefix(person, 'old_');
92 |
93 | oldPerson.old_age;
94 | oldPerson.old_location;
95 | oldPerson.old_name;
96 |
97 |
98 | ```
--------------------------------------------------------------------------------
/docs/type-programming/loop/recursive-types.md:
--------------------------------------------------------------------------------
1 | # Loop with Recursive Types
2 |
3 | In this section, we will explore how to create a loop with recursive types in TypeScript. We will start by defining a simple recursive type and then show how to create a loop with it.
4 |
5 | ## Recursive Types
6 |
7 | Basic recursive types are types that refer to themselves in their definition. For example, consider the following type definition:
8 |
9 | ```ts twoslash
10 | type Recursive = T extends [infer Head, ...infer Tail]
11 | ? [Head, ...Recursive]
12 | : [];
13 | ```
14 |
15 | In this definition, the `Recursive` type is defined in terms of itself. It takes a type `T` and checks if `T` extends an array with a head element and a tail element. If it does, it constructs a new array type by recursively applying `Recursive` to the tail element.
16 |
17 | ## Examples
18 |
19 | ```ts twoslash
20 | // This case Tail always be array
21 | type CheckTailStatus = T extends [infer H, ...infer T]
22 | ? T extends any[]
23 | ? 'Tail is array'
24 | : 'Tail is not array'
25 | : 'empty array';
26 |
27 | // The result will be "empty array"
28 | type check1 = CheckTailStatus<[]>;
29 | // The result will be "Tail is array"
30 | type check2 = CheckTailStatus<['hello']>;
31 | // The result will be "Tail is array"
32 | type check3 = CheckTailStatus<['hello', 'wolrd']>;
33 | ```
34 |
35 | In this example, we define a type `CheckTailStatus` that checks if the tail of an array is an array or not. We use conditional types to check if the tail element `T` extends an array. If it does, we return `'Tail is array'`; otherwise, we return `'Tail is not array'`.
--------------------------------------------------------------------------------
/docs/type-programming/testing.md:
--------------------------------------------------------------------------------
1 | # Testing
2 |
3 | - https://www.npmjs.com/package/@type-challenges/utils
4 | - [Testing Types](https://vitest.dev/guide/testing-types) in Vitest
--------------------------------------------------------------------------------
/docs/type-programming/type-programming.md:
--------------------------------------------------------------------------------
1 | ---
2 | outline: deep
3 | ---
4 |
5 | # Type Programming
6 |
7 | Type programming or type-level programmming, definition from [Type-Level Programming](https://type-level-typescript.com/), some developer say about [the Turing Complete type system](https://github.com/microsoft/TypeScript/issues/14833) in Typescript. Whatever it is, in my book, I will describe as **Type Programming**.
8 |
9 | Type programming is the practice of writing programs that operate on types. In TypeScript, this is done using the type system. Type-level programming is a powerful technique that can be used to create complex type-level computations and to enforce constraints on types.
10 |
11 |
12 | ## Thinking Types as Variables
13 |
14 | This might blow your mind, but in type programming, we can think of types as variables. We can declare a type and assign it a value. For example, we can declare a type `Age` and assign it the value `30`. This is similar to declaring a variable `age` and assigning it the value `30`.
15 |
16 | ```ts
17 | // Declare a variable
18 | const age = 30;
19 | // Declare a type
20 | type Age = 30;
21 | ```
22 |
23 | ## Mapping Programming to Type
24 | | Generally Programming | Type Programming |
25 | | --------------------- | ---------------- |
26 | | Variable, Collection | Type |
27 | | Object | Interface |
28 | | Function | Generic Type |
29 | | If-Else | Conditional Type |
30 | | Loop | Recursive Type |
31 |
32 | Why we don't call type programming with the similar name as programming? In my opinion, it's because the type programming is not a programming language. It's a type system that is used to enforce constraints on types. It might be confuse when call the same thing. In programming, we use variables to store values. In type programming, we use types to store constraints on values.
33 |
34 | ### Variable, Collection -> Type
35 |
36 | In generally programming, a variable is a storage location and an associated symbolic name (an identifier) which contains some known or unknown quantity of information referred to as a value. The variable name is the way we refer to the value stored in the variable.
37 |
38 | In type programming, we can think of a type as a variable. We can declare a type and assign it a value. For example, we can declare a type `Age` and assign it the value `30`. This is similar to declaring a variable `age` and assigning it the value `30`.
39 |
40 | ```ts
41 | // Declare a variable
42 | const age = 30;
43 | // Declare a type
44 | type Age = 30;
45 | ```
46 |
47 | For collection, in generally programming, a collection is a group of objects or values that are stored together. In type programming, we can think of a type as a collection. We can declare a type that contains a group of values. For example, we can declare a type `Numbers` that contains the values `1`, `2`, and `3`. This is similar to declaring an array `numbers` that contains the values `1`, `2`, and `3`.
48 |
49 | ```ts
50 | // Declare an array
51 | const numbers = [1, 2, 3];
52 | // Declare a type
53 | type Numbers = 1 | 2 | 3;
54 | ```
55 |
56 | Don't be confused, in type programming, we can't store values in a type. The data type of `numbers` is an array of numbers (`number[]`).
57 |
58 |
59 | ### Object -> Interface
60 |
61 | In generally programming, an object is a collection of properties, where each property has a name and a value. In type programming, we can think of an interface as an object. We can declare an interface that contains properties with names and values.
62 |
63 | For example, we can declare an interface `Person` with properties `name` and `age`. This is similar to declaring an object `person` with properties `name` and `age`.
64 |
65 | ```ts
66 | // Declare an object
67 | const person = {
68 | name: 'Alice',
69 | age: 30,
70 | };
71 | // Declare an interface
72 | interface Person {
73 | name: 'Alice';
74 | age: 30;
75 | }
76 | ```
77 |
78 | ### Function -> Generic Type
79 |
80 | In generally programming, a function is a block of code that performs a specific task. In type programming, we can think of a generic type as a function. We can declare a generic type that takes input types and returns output types.
81 |
82 | For example, we can declare a generic type `Concat` that takes two input types `A` and `B` and returns an output type `Result`. This is similar to declaring a function `concat` that takes two input values `a` and `b` and returns an output value `a + b`.
83 |
84 | ```ts twoslash
85 | // Declare a function
86 | function concat(a: string, b: string): string {
87 | return `${a}${b}`;
88 | }
89 | // Declare a generic type
90 | type Concat = `${A}${B}`;
91 |
92 | // Test the generic type
93 | type Result = Concat<'Hello', 'World'>;
94 | // ^?
95 |
96 | // End of the example
97 | ```
98 |
99 | ### If-Else -> Conditional Type
100 |
101 | In generally programming, an if-else statement is a control flow statement that allows us to execute different code blocks based on a condition. In type programming, we can think of a conditional type as an if-else statement. We can declare a conditional type that checks a condition and returns different types based on the condition.
102 |
103 | For example, we can declare a conditional type `IsString` that checks if a type `T` is a string or not. This is similar to declaring an if-else statement that checks if a value is a string or not.
104 |
105 | ```ts twoslash
106 | // Declare a conditional type
107 | type IsString = T extends string ? 'Yes' : 'No';
108 |
109 | // Test the conditional type
110 | type Test1 = IsString; // 'Yes'
111 | type Test2 = IsString; // 'No'
112 | ```
113 |
114 | ::: warning
115 | The `extends` keyword in the conditional type is used to check if a type `T` extends that type,
116 |
117 | From example, `T extends string` checks if a type `T` is assignable to a type `string`, in the other hands, you can think `T` is a subset of `string`.
118 |
119 | This is not equivalent to `===` operator in JavaScript. The `extends` keyword in the conditional type is used to check if a type `T` is assignable to a type `string`.
120 | :::
121 |
122 | ### Loop -> Recursive Type
123 |
124 | In generally programming, a loop is a control flow statement that allows us to repeat a block of code multiple times. In type programming, we can think of a recursive type as a loop. We can declare a recursive type that repeats a type computation multiple times.
125 |
126 | For example, we can declare a recursive type `FilterString` that filters out the string elements from an array type `T`. This is similar to writing a loop that filters out the string elements from an array.
127 |
128 | ```ts twoslash
129 | // Declare a recursive type
130 | type FilterString = T extends [infer Head, ...infer Tail]
131 | ? Head extends string
132 | ? [Head, ...FilterString]
133 | : FilterString
134 | : [];
135 |
136 | // Test the recursive type
137 | type Test1 = FilterString<[1, 'hello', 2, 'world', 3]>; // ['hello', 'world'];
138 | type Test2 = FilterString<[1, 2, 3]>; // []
139 | ```
140 |
141 | ### Summary
142 |
143 | Type in TypeScript are similar to programming constructs in other languages. We can think of types as variables, interfaces as objects, generic types as functions, conditional types as if-else statements, and recursive types as loops. By thinking of types as variables, we can write type-level programs that operate on types.
144 |
145 | However, some operators in type programming are not available in programming languages. For example, we can use the `keyof` operator to get the keys of an object type, the `infer` keyword to infer the type of a generic type, and the `never` type to represent an unreachable code path. These operators are unique to TypeScript and are not available in other programming languages.
146 |
147 | Moreover, in case of `extends` keyword in conditional type, it's not equivalent to `===` operator in JavaScript. The `extends` keyword in the conditional type is used to check if a type `T` is assignable to a type `string`.
148 |
149 | ## Recommended Resources
150 |
151 | - [type-challenges](https://github.com/type-challenges/type-challenges) Collection of TypeScript type challenges with online judge
152 | - [TypeScript Awesome Template Literal Types](https://github.com/ghoullier/awesome-template-literal-types)
153 | - [Type-Level Programming](https://type-level-typescript.com/)
154 | - [Extreme Explorations of TypeScript's Type System](https://www.learningtypescript.com/articles/extreme-explorations-of-typescripts-type-system) by Josh Goldberg
155 | - [TypeScript Type System Hacks](https://matt-rickard.com/typescript-type-system-hacks) by Matt Rickard
--------------------------------------------------------------------------------
/docs/what-type-safe.md:
--------------------------------------------------------------------------------
1 | # What is Type-safe?
2 |
3 | ::: tip TL;DR:
4 | **Type-safe** ensures data types are correct and consistent in programs, especially in modern languages like TypeScript. Key characteristics include using generic and [template literal types](./basic-types/template-literal-types), the builder pattern, automatic type inference, and reducing type duplication. Examples of TypeScript projects utilizing type-safe design include Zod, tRPC, Hono, and Elysia.
5 | :::
6 |
7 | Many in the programming community frequently mention **"type-safe,"** yet few attempt to define it clearly. For more clarity, let's break down what type-safe means in the context of modern programming languages like TypeScript in my understanding.
8 |
9 | Type-safe is a design pattern that ensures data types are correct and consistent within a program. This pattern is particularly valuable in modern programming languages like TypeScript, which boasts a powerful type system to enhance program correctness and reliability.
10 |
11 | ## The Traditional Type
12 |
13 | When I've mentioned the traditional type, I mean the type that is defined in the program. Not only TypeScript type, but also the type in other programming language like Java, C#, etc.
14 |
15 | The traditional type is a way to define the data type in the program. It can be a primitive type like `number`, `string`, `boolean`, etc. or a complex type like `interface`, `class`, etc.
16 |
17 | For example, in TypeScript, we can define a type like this:
18 |
19 | ```ts twoslash
20 | interface Person {
21 | name: string;
22 | age: number;
23 | }
24 | ```
25 |
26 | ## Characteristics of Type-safe
27 |
28 | Many libraries and frameworks provide type-safe support, here is some characteristics of type-safe when the they claim that they are type-safe:
29 |
30 | - **Generic**: They use generic types
31 | - **Template Literal Types**: They use template literal types
32 | - **Builder Pattern**: They use builder pattern
33 | - **Automatic Type Inference**: They use automatic type inference passing through the function and generic types
34 | - **Reduce Type Duplicate Definition**: They reduce the duplicate definition of types
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "book-type-safe-design-pattern",
3 | "module": "index.ts",
4 | "type": "module",
5 | "devDependencies": {
6 | "@azure/static-web-apps-cli": "^1.1.7",
7 | "@shikijs/vitepress-twoslash": "^1.5.2",
8 | "@thaitype/vitepress-typed-navbar": "^1.0.1",
9 | "@type-challenges/utils": "^0.1.1",
10 | "@types/bun": "latest",
11 | "@types/node": "^20.12.12",
12 | "ts-odata-client": "2.0.2",
13 | "vitepress": "^1.2.2",
14 | "vue": "^3.4.27"
15 | },
16 | "peerDependencies": {
17 | "typescript": "^5.0.0"
18 | },
19 | "scripts": {
20 | "dev": "vitepress dev docs",
21 | "build": "vitepress build docs",
22 | "deploy": "swa deploy ./docs/.vitepress/dist --env production -n thadaw-type-safe",
23 | "deploy:staging": "swa deploy ./docs/.vitepress/dist --env staging -n thadaw-type-safe",
24 | "preview": "vitepress preview docs"
25 | }
26 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Enable latest features
4 | "lib": ["ESNext", "DOM"],
5 | "target": "ESNext",
6 | "module": "ESNext",
7 | "moduleDetection": "force",
8 | "jsx": "react-jsx",
9 | "allowJs": true,
10 |
11 | // Bundler mode
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "verbatimModuleSyntax": true,
15 | "noEmit": true,
16 |
17 | // Best practices
18 | "strict": true,
19 | "skipLibCheck": true,
20 | "noFallthroughCasesInSwitch": true,
21 |
22 | // Some stricter flags (disabled by default)
23 | "noUnusedLocals": false,
24 | "noUnusedParameters": false,
25 | "noPropertyAccessFromIndexSignature": false
26 | }
27 | }
28 |
--------------------------------------------------------------------------------