Basic Types and Annotations
Primitives, arrays, objects, and function annotations TypeScript actually checks — plus where inference is enough and where it is not.
- typescript
- basic-types
TypeScript’s value for frontend work starts with basic types and annotations: saying what a value is so the compiler can refuse nonsense before runtime. You do not need advanced mapped types on day one. You need honest string / number / boolean, typed objects, and function signatures that match how the UI actually works.
Docs: Everyday Types, Narrowing.
Primitives you will use daily
const id: string = 'user_1';
const count: number = 0;
const ready: boolean = false;
const empty: null = null;
const missing: undefined = undefined;
In practice you rarely annotate locals when the right-hand side is obvious — inference fills them in:
const id = 'user_1'; // string
const count = 0; // number
Annotate when:
- The value is uninitialized and filled later.
- You want a wider type than the literal (
let status: string = 'idle'vs literal'idle'). - The value crosses a module boundary (exports, public function params).
let status: 'idle' | 'loading' | 'done' = 'idle';
// later
status = 'loading';
Arrays and tuples (peek)
const tags: string[] = ['ts', 'react'];
const pair: [string, number] = ['retries', 3];
string[] is “list of strings.” A tuple fixes length and position types — useful for coordinates, CSV rows, and React useState pairs in library code. Prefer arrays for open-ended lists.
Object types
type User = {
id: string;
name: string;
email?: string; // optional
};
function greet(user: User): string {
return `Hi ${user.name}`;
}
Optional (?) means the property may be missing — not the same as email: string | null (present but nullable). Be explicit about which you mean for API payloads.
Index signatures when keys are dynamic:
type Scores = { [userId: string]: number };
Prefer Record<string, number> for the same idea in modern codebases.
Function annotations
function add(a: number, b: number): number {
return a + b;
}
const onSave = (value: string): void => {
localStorage.setItem('draft', value);
};
Return type annotation is optional when inference is obvious; keep it on exported functions so refactors fail loudly at the signature.
Callbacks in React:
type Props = {
onSelect: (id: string) => void;
items: string[];
};
any vs leaving it untyped
Unannotated parameters under noImplicitAny are errors. That is intentional. Prefer:
function parse(raw: unknown) {
// narrow before use
}
over raw: any. See unknown vs any.
Type annotations vs assertions
const el = document.querySelector('#app'); // Element | null
const app = document.querySelector('#app') as HTMLDivElement; // assertion — trust me
Annotations declare intent; assertions override the checker. Prefer narrowing:
const el = document.querySelector('#app');
if (!(el instanceof HTMLDivElement)) throw new Error('missing #app');
// el is HTMLDivElement
Union types early
Frontend state is rarely one shape:
type LoadState =
| { status: 'idle' }
| { status: 'error'; message: string }
| { status: 'ok'; data: User };
Basic annotations + unions beat optional soup (data?: User; error?: string). More: discriminated unions for UI state, union and intersection types.
Common footguns
| Footgun | Fix |
|---|---|
| Annotating every local | Let inference work; annotate APIs |
Object / {} for “any object” |
Prefer concrete types or Record<string, unknown> |
number for IDs from JSON |
IDs are often string; don’t coerce casually |
Forgetting null from DOM APIs |
Handle null or throw |
Array<any> |
unknown[] + map with guards |
// Weak
function log(x: Object) {
console.log(x);
}
// Stronger boundary
function logUnknown(x: unknown) {
console.log(x);
}
Mental model for interviews
TypeScript annotations are contracts. Primitives and object types describe values; function types describe call sites. Inference is the default; write types where ambiguity or public API risk is real. Strict mode without annotations discipline is still “JavaScript with a false sense of safety.”
Interview out-loud answer
“I annotate public APIs and state that has more than one shape. Locals usually infer. I avoid any, use unknown at boundaries, and prefer unions over optional fields for mutually exclusive UI states. Assertions are last resort after a runtime check.”
Related on this site
Further reading
Related guides
- Branded Types for IDsNominal-style UserId vs OrderId in TypeScript — prevent ID mixups at compile time with brands, parsers, and form boundaries.
- Conditional Types IntroT extends U ? X : Y — how TypeScript picks types from conditions, distributes over unions, and powers utility types you already use.
- Declaration Files and DefinitelyTypedHow .d.ts files describe JS to TypeScript, when to use @types packages, module augmentation, and writing minimal ambient types for untyped libs.
- Discriminated Unions for UI StateModel loading, success, and error as mutually exclusive variants so TypeScript and your UI cannot show impossible states.
- Enums vs Union TypesWhen TypeScript enums help, when they hurt, and why string literal unions plus const objects win for most frontend code.