Lesson 05-TypeScript Project Architecture and Engineering

Project Architecture Design

Single Page Application (SPA) and Multi-Page Application Architecture

Core Design of SPA Architecture:

// Typical SPA Project Structure
src/
├── assets/          # Static assets
├── components/      # Common components
├── pages/           # Page components
├── routes/          # Route configurations
├── store/           # State management
├── services/        # API services
├── utils/           # Utility functions
├── App.tsx          # Root component
└── main.tsx         # Entry file
// Route Configuration Example (React Router v6)
import { BrowserRouter, Routes, Route } from 'react-router-dom'

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/about" element={<AboutPage />} />
        <Route path="/dashboard" element={<PrivateRoute><Dashboard /></PrivateRoute>} />
      </Routes>
    </BrowserRouter>
  )
}

Key Points of MPA Architecture Design:

// Multi-Page Application Configuration Example (Webpack)
module.exports = {
  entry: {
    home: './src/pages/home/index.tsx',
    about: './src/pages/about/index.tsx'
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  // ...other configurations
}

// Page Entry File Example
// src/pages/home/index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import HomePage from '../../components/HomePage'

ReactDOM.render(<HomePage />, document.getElementById('root'))

Modular and Component-Based Design

Modular Design Principles:

// Modular Example: User Management Module
src/
├── modules/
│   ├── user/
│   │   ├── components/  # User-related components
│   │   ├── api/         # User API services
│   │   ├── types/       # User-related type definitions
│   │   ├── hooks/       # User-related custom hooks
│   │   └── index.ts     # Module export entry
// Type Definition Example
// src/modules/user/types.ts
export interface User {
  id: number
  name: string
  email: string
}

export type UserListResponse = PaginatedResponse<User>

// API Service Example
// src/modules/user/api.ts
import axios from 'axios'
import { User, UserListResponse } from './types'

export const fetchUsers = async (page: number): Promise<UserListResponse> => {
  const response = await axios.get(`/api/users?page=${page}`)
  return response.data
}

Component-Based Design Specifications:

// Component Design Example
// src/components/Button/index.tsx
import React from 'react'
import PropTypes from 'prop-types'

interface ButtonProps {
  variant?: 'primary' | 'secondary'
  size?: 'sm' | 'md' | 'lg'
  onClick?: () => void
  children: React.ReactNode
}

const Button: React.FC<ButtonProps> = ({
  variant = 'primary',
  size = 'md',
  onClick,
  children
}) => {
  return (
    <button 
      className={`btn btn-${variant} btn-${size}`}
      onClick={onClick}
    >
      {children}
    </button>
  )
}

Button.propTypes = {
  variant: PropTypes.oneOf(['primary', 'secondary']),
  size: PropTypes.oneOf(['sm', 'md', 'lg']),
  onClick: PropTypes.func,
  children: PropTypes.node.isRequired
}

export default Button

State Management Architecture

Redux Toolkit Configuration Example:

// store/configureStore.ts
import { configureStore } from '@reduxjs/toolkit'
import userReducer from '../modules/user/slice'

export const store = configureStore({
  reducer: {
    user: userReducer
  },
  devTools: process.env.NODE_ENV !== 'production'
})

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

// Modular Slice Example
// modules/user/slice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { User } from '../types'

interface UserState {
  currentUser: User | null
  loading: boolean
  error: string | null
}

const initialState: UserState = {
  currentUser: null,
  loading: false,
  error: null
}

const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    loginStart(state) {
      state.loading = true
      state.error = null
    },
    loginSuccess(state, action: PayloadAction<User>) {
      state.currentUser = action.payload
      state.loading = false
    },
    loginFailure(state, action: PayloadAction<string>) {
      state.error = action.payload
      state.loading = false
    }
  }
})

export const { loginStart, loginSuccess, loginFailure } = userSlice.actions
export default userSlice.reducer

MobX State Management Example:

// stores/UserStore.ts
import { makeAutoObservable } from 'mobx'
import { User } from '../modules/user/types'
import { fetchUser } from '../modules/user/api'

class UserStore {
  currentUser: User | null = null
  loading = false
  error: string | null = null

  constructor() {
    makeAutoObservable(this)
  }

  async login(userId: number) {
    this.loading = true
    this.error = null
    try {
      const user = await fetchUser(userId)
      this.currentUser = user
    } catch (err) {
      this.error = err.message
    } finally {
      this.loading = false
    }
  }
}

export const userStore = new UserStore()

Routing Architecture Design

Advanced React Router Configuration:

// routes/index.tsx
import { lazy, Suspense } from 'react'
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import LoadingSpinner from '../components/LoadingSpinner'

const Home = lazy(() => import('../pages/Home'))
const About = lazy(() => import('../pages/About'))
const Dashboard = lazy(() => import('../pages/Dashboard'))

const AppRoutes = () => {
  return (
    <BrowserRouter>
      <Suspense fallback={<LoadingSpinner />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route 
            path="/dashboard" 
            element={
              <RequireAuth>
                <Dashboard />
              </RequireAuth>
            } 
          />
          <Route path="*" element={<Navigate to="/" replace />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  )
}

// Authentication Control Component
const RequireAuth = ({ children }: { children: JSX.Element }) => {
  const { currentUser } = useUserStore()
  if (!currentUser) {
    return <Navigate to="/login" replace />
  }
  return children
}

Vue Router Configuration Example:

// router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Home from '../views/Home.vue'

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('../views/About.vue') // Lazy loading
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('../views/Dashboard.vue'),
    meta: { requiresAuth: true }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

// Global Navigation Guard
router.beforeEach((to, from, next) => {
  const isAuthenticated = store.getters['user/isAuthenticated']
  if (to.meta.requiresAuth && !isAuthenticated) {
    next('/login')
  } else {
    next()
  }
})

export default router

Project Directory Structure Specifications

Recommended Project Structure:

my-project/
├── .github/                  # GitHub configurations
│   └── workflows/            # CI/CD workflows
├── .husky/                   # Husky configurations
├── public/                   # Static assets (not processed by Webpack)
│   ├── favicon.ico
│   └── index.html
├── src/
│   ├── assets/               # Processed static assets
│   ├── components/           # Common components
│   │   ├── ui/               # Basic UI components
│   │   └── layout/           # Layout components
│   ├── hooks/                # Custom hooks
│   ├── modules/              # Feature modules
│   │   ├── user/             # User module
│   │   └── product/          # Product module
│   ├── router/               # Route configurations
│   ├── services/             # API services
│   ├── store/                # State management
│   ├── types/                # Global type definitions
│   ├── utils/                # Utility functions
│   ├── App.tsx               # Root component
│   └── main.tsx              # Entry file
├── tests/                    # Test files
├── .editorconfig             # Editor configuration
├── .eslintrc.js              # ESLint configuration
├── .gitignore                # Git ignore file
├── .prettierrc               # Prettier configuration
├── package.json              # Project dependencies
├── tsconfig.json             # TypeScript configuration
└── vite.config.ts            # Vite configuration (or webpack.config.js)

Directory Structure Design Principles:

  1. Functional Modularization: Organize by business features
  2. Clear Layering: Separate components, state, routes, and services
  3. Centralized Type Management: Store global types separately
  4. Test-Friendly: Keep test files alongside source code
  5. Configuration Separation: Manage build configurations independently

Engineering Toolchain

Advanced TypeScript Configuration

In-Depth tsconfig Configuration:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["DOM", "DOM.Iterable", "ES2020"],
    "moduleResolution": "Node",
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },
    "types": ["vite/client"],
    "declaration": true,
    "declarationDir": "dist/types",
    "emitDeclarationOnly": false,
    "composite": true,
    "incremental": true,
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.spec.ts"]
}

Project References Configuration:

// tsconfig.json (root project)
{
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/utils" }
  ]
}

// packages/core/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "outDir": "../../dist/core"
  },
  "include": ["src"]
}

// packages/utils/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "outDir": "../../dist/utils"
  },
  "include": ["src"]
}

Declaration File Generation

Automatic d.ts File Generation:

// tsconfig.json
{
  "compilerOptions": {
    "declaration": true,
    "declarationDir": "dist/types",
    "emitDeclarationOnly": false
  }
}

// Generate Declaration Files Command
tsc --emitDeclarationOnly

// Third-Party Library Type Declaration Example
// types/custom.d.ts
declare module 'untyped-library' {
  export function doSomething(): void
  export const someValue: number
}

// Global Type Extension
// types/global.d.ts
declare global {
  interface Window {
    __MY_APP_CONFIG__: {
      apiBaseUrl: string
    }
  }
}

Type Checking and Code Quality

ESLint + Prettier Configuration:

// .eslintrc.js
module.exports = {
  root: true,
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: 'module',
    project: './tsconfig.json'
  },
  plugins: ['@typescript-eslint', 'prettier'],
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended'
  ],
  rules: {
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/no-explicit-any': 'warn',
    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
    'prettier/prettier': 'error'
  }
}

// .prettierrc
{
  "printWidth": 100,
  "tabWidth": 2,
  "semi": true,
  "singleQuote": true,
  "trailingComma": "all",
  "bracketSpacing": true
}

Husky + lint-staged Configuration:

// package.json
{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",
      "prettier --write",
      "git add"
    ]
  }
}

Code Splitting and Lazy Loading

Dynamic Import Implementation:

// React Dynamic Import Example
import React, { Suspense, lazy } from 'react'

const LazyComponent = lazy(() => import('./components/HeavyComponent'))

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  )
}

// Vue Dynamic Import Example
const LazyComponent = () => import('./components/HeavyComponent.vue')

// Route-Level Code Splitting
const routes = [
  {
    path: '/dashboard',
    component: () => import('./views/Dashboard.vue')
  }
]

// Webpack Magic Comments
const LazyComponent = lazy(() => import(
  /* webpackChunkName: "dashboard" */
  /* webpackPrefetch: true */
  './components/Dashboard'
))

Vite Dynamic Import:

// Vite Dynamic Import (Automatic Code Splitting)
const module = await import('./path/to/module')

// Preloading Strategy
import.meta.glob('./modules/*.ts')

Type-Safe API Encapsulation

Axios Type-Safe Encapsulation:

// services/api.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'

interface ApiResponse<T> {
  code: number
  data: T
  message: string
}

class ApiClient {
  private instance: AxiosInstance

  constructor(baseURL: string) {
    this.instance = axios.create({
      baseURL,
      timeout: 10000
    })

    this.setupInterceptors()
  }

  private setupInterceptors() {
    this.instance.interceptors.request.use(
      (config) => {
        // Add authentication token, etc.
        const token = localStorage.getItem('token')
        if (token) {
          config.headers.Authorization = `Bearer ${token}`
        }
        return config
      },
      (error) => Promise.reject(error)
    )

    this.instance.interceptors.response.use(
      (response: AxiosResponse<ApiResponse<any>>) => {
        if (response.data.code !== 200) {
          // Handle business errors
          return Promise.reject(new Error(response.data.message))
        }
        return response.data.data
      },
      (error) => {
        // Handle HTTP errors
        return Promise.reject(error)
      }
    )
  }

  public async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.get<ApiResponse<T>>(url, config)
  }

  public async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.post<ApiResponse<T>>(url, data, config)
  }
}

// Create API Client Instance
export const apiClient = new ApiClient(process.env.API_BASE_URL!)

// API Service Example
// services/userApi.ts
import { apiClient } from './api'

interface User {
  id: number
  name: string
  email: string
}

export const userApi = {
  getUsers: () => apiClient.get<User[]>('/users'),
  getUserById: (id: number) => apiClient.get<User>(`/users/${id}`),
  createUser: (userData: Omit<User, 'id'>) => apiClient.post<User>('/users', userData)
}

GraphQL Type-Safe Encapsulation:

// services/graphql.ts
import { ApolloClient, InMemoryCache, gql } from '@apollo/client/core'

const cache = new InMemoryCache()

export const client = new ApolloClient({
  uri: 'https://api.example.com/graphql',
  cache
})

// Type-Safe Query
interface User {
  id: number
  name: string
  email: string
}

interface GetUsersResponse {
  users: User[]
}

export const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
    }
  }
`

export async function fetchUsers(): Promise<GetUsersResponse> {
  const { data } = await client.query<GetUsersResponse>({ query: GET_USERS })
  return data
}

Performance Optimization and Deployment

Compilation Speed Optimization

Incremental Compilation Configuration:

// tsconfig.json
{
  "compilerOptions": {
    "incremental": true,
    "tsBuildInfoFile": "./.tsbuildinfo"
  }
}

// Project References for Build Optimization
{
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/utils" }
  ]
}

// Optimized Build Command
tsc --build --watch

Vite Build Optimization:

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  build: {
    minify: 'terser',
    sourcemap: false,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          utils: ['lodash', 'date-fns']
        }
      }
    }
  }
})

Type Checking Performance Optimization

Skipping Declaration File Checks:

// tsconfig.json
{
  "compilerOptions": {
    "skipLibCheck": true
  }
}

// Exclude Specific Files from Checking
{
  "exclude": ["**/*.d.ts", "**/node_modules/**"]
}

// Modular Checking
tsc --project tsconfig.core.json
tsc --project tsconfig.utils.json

Caching Strategy:

# Use swc instead of tsc for type checking (experimental)
SWC_NODE_PROJECT=./tsconfig.json swc src -d dist

# Custom Cache Implementation with TypeScript Compiler API
import * as ts from 'typescript'
import fs from 'fs'

function checkWithCache(file: string) {
  const cacheFile = `.cache/${file}.json`
  if (fs.existsSync(cacheFile)) {
    const cached = JSON.parse(fs.readFileSync(cacheFile, 'utf8'))
    if (cached.hash === getFileHash(file)) {
      return cached.diagnostics
    }
  }
  
  const program = ts.createProgram([file], {})
  const diagnostics = program.getSemanticDiagnostics()
  
  fs.writeFileSync(cacheFile, JSON.stringify({
    hash: getFileHash(file),
    diagnostics
  }))
  
  return diagnostics
}

Packaging and Distribution

Webpack Production Configuration:

// webpack.prod.js
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
const TerserPlugin = require('terser-webpack-plugin')

module.exports = merge(common, {
  mode: 'production',
  devtool: false,
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          compress: {
            drop_console: true
          }
        }
      })
    ],
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  },
  performance: {
    hints: 'warning',
    maxEntrypointSize: 512000,
    maxAssetSize: 512000
  }
})

Rollup Configuration Example:

// rollup.config.js
import typescript from '@rollup/plugin-typescript'
import { nodeResolve } from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import { terser } from 'rollup-plugin-terser'

export default {
  input: 'src/index.ts',
  output: [
    {
      file: 'dist/bundle.cjs.js',
      format: 'cjs'
    },
    {
      file: 'dist/bundle.esm.js',
      format: 'esm'
    }
  ],
  plugins: [
    nodeResolve(),
    commonjs(),
    typescript({
      declaration: true,
      declarationDir: 'dist/types'
    }),
    terser()
  ]
}

Type-Safe Deployment Process

CI/CD Type Checking Integration:

# .github/workflows/ci.yml
name: CI Pipeline

on: [push, pull_request]

jobs:
  type-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '16'
      - run: npm ci
      - run: npm run type-check
      
  build:
    needs: type-check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '16'
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v2
        with:
          name: dist
          path: dist

  deploy:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/download-artifact@v2
        with:
          name: dist
      - run: npm run deploy

Docker Type-Safe Build:

# Dockerfile
FROM node:16-alpine as builder

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run type-check && npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Continuous Integration and Type Checking

Advanced GitHub Actions Configuration:

name: Advanced CI

on:
  push:
    branches: [main]
  pull_request:
    types: [opened, synchronize]

jobs:
  setup:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    steps:
      - uses: actions/checkout@v2
      - id: set-matrix
        run: echo "::set-output name=matrix::{\"include\":[{\"module\":\"core\"},{\"module\":\"utils\"}]}"
  
  type-check:
    needs: setup
    runs-on: ubuntu-latest
    strategy:
      matrix: ${{fromJson(needs.setup.outputs.matrix)}}
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '16'
      - run: npm ci
      - run: npm run type-check -- --project packages/${{ matrix.module }}/tsconfig.json
      
  test:
    needs: type-check
    runs-on: ubuntu-latest
    strategy:
      matrix: ${{fromJson(needs.setup.outputs.matrix)}}
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '16'
      - run: npm ci
      - run: npm test -- packages/${{ matrix.module }}
      
  deploy:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '16'
      - run: npm ci
      - run: npm run build
      - run: npm run deploy

CircleCI Configuration Example:

# .circleci/config.yml
version: 2.1

jobs:
  type-check:
    docker:
      - image: cimg/node:16.14
    steps:
      - checkout
      - restore_cache:
          keys: [v1-deps-{{ checksum "package-lock.json" }}]
      - run: npm ci
      - save_cache:
          paths: [node_modules]
          key: v1-deps-{{ checksum "package-lock.json" }}
      - run: npm run type-check
      
  build:
    docker:
      - image: cimg/node:16.14
    steps:
      - checkout
      - restore_cache:
          keys: [v1-deps-{{ checksum "package-lock.json" }}]
      - run: npm ci
      - run: npm run build
      - save_cache:
          paths: [dist]
          key: v1-dist-{{ .Environment.CIRCLE_SHA1 }}
      
  deploy:
    docker:
      - image: cimg/base:2021.09
    steps:
      - checkout
      - restore_cache:
          keys: [v1-dist-{{ .Environment.CIRCLE_SHA1 }}]
      - run: npm run deploy

workflows:
  version: 2
  ci_pipeline:
    jobs:
      - type-check
      - build:
          requires: [type-check]
      - deploy:
          requires: [build]
          filters:
            branches:
              only: main
Share your love