A simple todo app where each operation (add, delete, update) is handled via pure functions.

A purely functional TypeScript todo app without HTML markup directly involves rendering entirely through functions.
Build each UI component functionally and assemble them in the app without relying on traditional HTML markup.

1. Define Types: Define types to represent each component and application state.

// src/types.ts
export type Todo = {
id: number;
text: string;
completed: boolean;
};
export type State = {
todos: Todo[];
};

2. Create Todo Operations as Pure Functions: Define functions to handle adding, toggling, and deleting todos, keeping them immutable.

// src/todoActions.ts
import { State } from './types';

export const addTodo = (state: State, text: string): State => ({
...state,
todos: [
...state.todos,
{ id: Date.now(), text, completed: false },
],
});

export const toggleTodo = (state: State, id: number): State => ({
...state,
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
});

export const deleteTodo = (state: State, id: number): State => ({
...state,
todos: state.todos.filter(todo => todo.id !== id),
});

3. Functional UI Component Functions: Each component (title, input form, and todo list) will be created by functional code that programmatically generates HTML elements. We’ll use helper functions to create the elements and apply the state changes functionally.

// src/components.ts
import { Todo } from './types'; // Keep Todo if you're using it in type annotations for props
export const createTitle = (): HTMLElement => {
const title = document.createElement('h1');
title.textContent = 'Todo List';
return title;
};

export const createAddTodoForm = (onSubmit: (text: string) => void): HTMLElement => {
const form = document.createElement('form');
const input = document.createElement('input');
const button = document.createElement('button');

input.type = 'text';
input.placeholder = 'Enter new todo';
button.type = 'submit';
button.textContent = 'Add';

form.appendChild(input);
form.appendChild(button);

form.onsubmit = (e) => {
e.preventDefault();
if (input.value.trim()) {
onSubmit(input.value);
input.value = '';
}
};

return form;
};

export const createTodoList = (todos: Todo[], onToggle: (id: number) => void, onDelete: (id: number) => void): HTMLElement => {
const list = document.createElement('ul');
todos.forEach(todo => {
const listItem = document.createElement('li');
listItem.textContent = todo.text;
listItem.className = todo.completed ? 'completed' : '';

const toggleButton = document.createElement('button');
toggleButton.textContent = todo.completed ? 'Undo' : 'Complete';
toggleButton.onclick = () => onToggle(todo.id);

const deleteButton = document.createElement('button');
deleteButton.textContent = 'Delete';
deleteButton.onclick = () => onDelete(todo.id);

listItem.appendChild(toggleButton);
listItem.appendChild(deleteButton);
list.appendChild(listItem);
});
return list;
};

4. UI Functionally - Create the App Renderer: The renderApp function clears and updates the DOM with new state every time the state changes.
Set up a simple UI. With Vite, you can use vanilla HTML and TypeScript.

// src/main.ts
import { createTitle, createAddTodoForm, createTodoList } from './components';
import { addTodo, toggleTodo, deleteTodo } from './todoActions';
import { State } from './types';
import './style.css';

let state: State = { todos: [] };

const renderApp = () => {
const root = document.getElementById('app');
if (!root) return;
root.innerHTML = '';

const title = createTitle();
const addTodoForm = createAddTodoForm((text: string) => {
state = addTodo(state, text);
renderApp();
});

const todoList = createTodoList(
state.todos,
(id: number) => {
state = toggleTodo(state, id);
renderApp();
},
(id: number) => {
state = deleteTodo(state, id);
renderApp();
}
);

root.appendChild(title);
root.appendChild(addTodoForm);
root.appendChild(todoList);
};

document.addEventListener('DOMContentLoaded', () => {
renderApp();
});

5. Basic HTML File:A minimal HTML file only includes a root element for our app to render dynamically

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo List</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/index.ts"></script>
</body>
</html>

Explanation:

1. Pure Component Functions:
createTitle, createAddTodoForm, and createTodoList are all pure functions generating DOM elements for each component.

2. State Updates:
The state remains immutable by replacing it each time through addTodo, toggleTodo, and deleteTodo.

3. Render Loop:
Every state update triggers renderApp, re-rendering the UI and ensuring it reflects the current state without directly manipulating the DOM outside of renderApp.

Add some CSS styling:
:root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;

color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;

font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

body {
margin: 0;
display: flex;
flex-direction: column;
align-items: center; /* Centers both #app and .app-notes horizontally */
min-width: 320px;
min-height: 100vh;
padding-top: 2rem; /* Add some spacing from the top */
}
/* app-notes is formatting for display of code explanations */
#app, .app-notes {
max-width: 1280px; width: 100%;
padding: 2rem;
text-align: left;
margin: 0 auto;
}

.app-notes {
margin-top: 1rem; /* Add space between #app and .app-notes */
}

a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}

a:hover {
color: #535bf2;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}

.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}

.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}

.logo.vanilla:hover {
filter: drop-shadow(0 0 2em #3178c6aa);
}

.card {
padding: 2em;
}

button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}

button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}

@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}

a:hover {
color: #747bff;
}

button {
background-color: #f9f9f9;
}
}