TypeScript in Practice: Types, Generics and Project Setup
TypeScript is a statically typed superset of JavaScript: it type-checks your program before it runs, catching the most common class of errors — using a value of one kind where another kind was expected. After years of growth, TypeScript has become the de facto standard for modern frontend engineering; the default scaffolds of React, Vue, and Next.js all ship with built-in support.
TypeScript is a core language skill under the frontend building category on 16IDC. For a broader tooling view, see the frontend toolchain 2026; to apply it in React or Vue, read the React 19 guide or the Vue 3 Composition API guide alongside.
Why Types Matter
As JavaScript scales, "what a function returns" and "which fields an object has" are only discoverable through runtime errors. The point of a type system is less about eliminating all bugs and more about moving errors earlier — from "after launch" to "while editing". The red squiggles in your editor, Ctrl+click to jump to a definition, and cross-file reference updates during refactors are all dividends of having type information. TypeScript doesn't change runtime behavior; it still compiles to standard JavaScript. In other words, types are a pure development-time asset, and how well you write them directly shapes your daily experience.
Basic Types and Narrowing
Beyond string, number, and boolean, common compositions include arrays T[], tuples [string, number], unions A | B, optional fields ?, and literal types such as "success". Combined with typeof/in/instanceof checks and type guards, you can narrow wide types into concrete ones at runtime — a key skill for robust code.
Take a function that accepts either a string or an object carrying a data field:
type Response =
| { status: "error"; message: string }
| { status: "ok"; data: User }
function handle(res: Response) {
if (res.status === "error") {
// narrowed here: res only has message
console.error(res.message)
} else {
// res only has data, typed as User
res.data.id
}
}
Equality narrowing over a discriminated union is the most common trick: use a discriminant field like status to split the union, and TypeScript infers the remaining fields in each branch. Once you get the hang of it, "null pointer"-style mistakes largely disappear.
Generics: Reusable Type Components
Generics let functions, interfaces, and classes work across many types without losing type information. Take the identity function:
function identity<Type>(arg: Type): Type {
return arg
}
let output = identity<string>("hello")
Constraints (extends) limit the shape of type parameters:
interface Lengthwise {
length: number
}
function logLength<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length)
return arg
}
Combined with keyof, conditional types, and mapped types, you can express complex relationships like key-to-value mappings with very little boilerplate. The generic you'll reach for most in real work is a shared request helper: fetchList<T>(url): Promise<T[]>. Call it as fetchList<User>("/api/users") and the return value is automatically typed as User[] — when the backend contract changes, the frontend finds out at compile time instead of at runtime.
Project Configuration: tsconfig.json
A well-tuned tsconfig.json makes type checking real. Recommended key options:
| Option | Purpose |
|---|---|
strict |
Enables all strict checks; teams should default to on |
target / module |
Compile target and module format |
moduleResolution |
Module resolution strategy (bundler suits modern tools like Vite) |
paths |
Path aliases such as @/* |
noUncheckedIndexedAccess |
Warns when indexed access may be undefined |
@ts-check also brings checking to plain JS files for gradual migration. strict is the single switch worth enabling from day one: it turns on a whole family of checks including strictNullChecks and noImplicitAny. Migrating old code will surface a pile of errors, but a greenfield project has no legacy baggage — just turn it on.
Working with Build Tools
TypeScript itself only checks types and transpiles; the actual bundling is left to build tools. With Vite, esbuild/oxc handles transpilation and tsc --noEmit handles type checking — a clean separation of concerns (see the Vite build tool guide). Many teams add a dedicated tsc --noEmit step to CI as a gate: code has to pass type checking before it merges, which beats relying on every developer's local discipline. It also helps to enable verbatimModuleSyntax in tsconfig so pure types are imported with import type; esbuild can then strip those imports outright, avoiding "importing a value that doesn't exist at runtime" bugs.
Adoption Advice
- Enable
strictby default: avoidanycreep from day one. - Type your public APIs: component props and API response shapes give the highest return.
- Be disciplined with
any: preferunknownplus narrowing. - Decouple types from runtime: use interface/type for data contracts and runtime validation libraries for external input.
Frequently Asked Questions
- What's the difference between
anyandunknown?anydrops all checking;unknownforces you to narrow before using it. Preferunknown. interfaceortype? Useinterfacefor object contracts (it merges and extends), andtypefor unions, intersections, and other compositions.- A third-party library has no types? Install
@types/xxxfirst; if none exists, declare a module withdeclare module— but don't just mark the whole library asany. - Compilation too slow? Use
tsc --noEmitpurely for checking and let esbuild/oxc bundle; on large projects enableincremental. - Too many errors after turning on
strict? Fix files one by one, using// @ts-expect-errorto keep the pipeline green while you clean up module by module — but don't disablestrictglobally just to pass checks.
Source: https://www.typescriptlang.org/docs/handbook/intro.html
Reference: https://www.typescriptlang.org/tsconfig/ (full tsconfig reference)