Keep reliable types at your application's boundaries, where the DOM, the network, and JSON escape the compiler.
Open this lesson in KodokonReal-world code starts where perfect types end: the DOM, the network, and JSON return data that TypeScript cannot guess. First frontier: the DOM. querySelector returns Element | null, a type too vague to read value or call focus().
const input = document.querySelector<HTMLInputElement>(
"#email"
);
if (input) {
input.value = "test@example.com";
input.focus();
}The type parameter is a promise you make to the compiler: it's up to you to target the right selector. When the element comes from an event, prefer an instanceof check, which actually verifies the type at runtime.
const form = document.querySelector("form");
form?.addEventListener("submit", (event) => {
event.preventDefault();
if (event.target instanceof HTMLFormElement) {
console.log(new FormData(event.target));
}
});Second frontier: the network. response.json() returns Promise<any>, in other words an open door to anything. The professional minimum: declare the expected type, check response.ok, and centralize the call in a typed function.
interface Post {
id: number;
title: string;
}
async function getPost(id: number): Promise<Post> {
const url = `https://api.example.com/posts/${id}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return (await response.json()) as Post;
}That as Post remains a statement of trust: if the API changes, nothing warns you at runtime. For critical data, write a type guard that validates the actual shape of the JSON before using it.
function isPost(value: unknown): value is Post {
if (typeof value !== "object" || value === null) {
return false;
}
const obj = value as Record<string, unknown>;
return (
typeof obj.id === "number" &&
typeof obj.title === "string"
);
}