JavaScript to TypeScript Variables
What changes (and what stays identical) when you add TypeScript annotations to JavaScript variables.
Type Annotations on let and const
The syntax for let and const does not change when you move to TypeScript. Every valid JavaScript variable declaration is also valid TypeScript. The difference is that TypeScript allows -- and sometimes requires -- a type annotation after the variable name: let count: number = 0. For simple literals, TypeScript's inference engine handles the type automatically, so the annotation is optional: let count = 0 infers number without you typing it. The annotation becomes useful when the initializer does not make the type obvious, such as values returned from third-party functions or JSON parse results. A practical rule of thumb: annotate when the inferred type would be any or when the inferred type is broader than you want (for instance, let items = [] infers never[], which breaks when you push to it). Explicit annotation there -- let items: string[] = [] -- tells the compiler what the array will hold. The muscle memory shift for JavaScript developers is minimal: most of the time, inference covers you, and you only intervene when the compiler asks or when clarity benefits your team.
Basic annotation vs inference
let count = 0;
const name = "Ada";
let items = [];let count: number = 0; // explicit (optional)
const name = "Ada"; // infers string
let items: string[] = []; // needed hereObject with type
const user = {
name: "Ada",
age: 30,
};type User = { name: string; age: number };
const user: User = {
name: "Ada",
age: 30,
};as const for Literal Types
JavaScript treats all string and number literals as their broad types: const direction = "north" gives typeof direction === "string" at the JavaScript level. TypeScript narrows const declarations automatically -- const direction = "north" infers the literal type "north", not string. But when objects or arrays are involved, inference widens: const config = { mode: "dark" } infers { mode: string }, not { mode: "dark" }. Adding as const to the declaration freezes the value at the narrowest possible type: const config = { mode: "dark" } as const infers { readonly mode: "dark" }. This assertion has no runtime effect -- it is erased during compilation -- but it changes how the type system treats the value. The pattern is especially useful for route maps, action type constants, and configuration objects where you want TypeScript to track exact string values rather than broad string. JavaScript developers will not find as const in their existing code; it is purely a TypeScript addition. The object itself behaves identically at runtime, but the compiler now rejects typos and mismatches that would otherwise slip through.
Widened vs narrow type
const config = {
mode: "dark",
retries: 3,
};
// typeof config.mode is just "string"const config = {
mode: "dark",
retries: 3,
} as const;
// typeof config.mode is "dark"
// typeof config.retries is 3Array as const
const roles = ["admin", "user", "guest"];
// roles[0] type is stringconst roles = ["admin", "user", "guest"] as const;
// roles[0] type is "admin"
// type Role = (typeof roles)[number]
// => "admin" | "user" | "guest"Type Inference in Practice
TypeScript's inference engine is aggressive enough that experienced TypeScript developers annotate surprisingly little. The return type of a function is inferred from its body. The type of a map/filter chain is inferred from the source array. Destructured variables inherit the types of the object being destructured. The practical impact is that migrating a JavaScript file to TypeScript often means renaming .js to .ts and fixing a handful of errors -- not annotating every line. Where inference falls short is boundaries: function parameters (always annotated in TypeScript), API response data (unknown until validated), and generic containers initialized empty. For these cases, you add annotations incrementally. The migration path is not "annotate everything first" but "let inference work and annotate the gaps." This bottom-up approach keeps the diff small and lets your team adjust gradually. One gotcha: enabling strict mode (strict: true in tsconfig.json) is strongly recommended because it turns on noImplicitAny, which flags variables where inference fails. Without strict mode, those variables silently become any, which defeats the purpose of adding TypeScript.
Inferred return type
function double(n) {
return n * 2;
}
// no way to know return type staticallyfunction double(n: number) {
return n * 2;
}
// return type inferred as numberDestructuring inherits types
const { name, age } = getUser();
// name and age are untypedconst { name, age } = getUser();
// name: string, age: number
// (inherited from getUser return type)Basic Types: string, number, boolean, Arrays
TypeScript's primitive types map directly to JavaScript's runtime types: string, number, boolean, null, undefined, symbol, bigint. The array type is expressed as string[] or Array<string> (both are equivalent). Object types use curly-brace syntax: { name: string; age: number }. These annotations exist only at the type level and are completely erased during compilation. The runtime code is identical to what you would write in plain JavaScript. One thing that catches JavaScript developers off guard: TypeScript distinguishes between string (primitive) and String (wrapper object). Always use lowercase string -- the uppercase form refers to the boxed object type and is almost never what you want. Similarly, number not Number, boolean not Boolean. Arrays have a subtle difference from JavaScript too: TypeScript's readonly modifier (readonly string[]) creates a type that disallows push, pop, and other mutations. The underlying array is still a regular JavaScript array at runtime, but the compiler blocks mutation at the type level. This is useful for function parameters where you want to promise callers that their array will not be modified.
Primitive types
let name = "Ada";
let age = 30;
let active = true;
let scores = [95, 87, 91];let name: string = "Ada";
let age: number = 30;
let active: boolean = true;
let scores: number[] = [95, 87, 91];Readonly array
function first(items) {
// nothing stops mutation
items.push("extra");
return items[0];
}function first(items: readonly string[]) {
// items.push("extra"); // compile error
return items[0];
}Can you write this from memory?
Check if string `item_text` matches the pattern `/abc/`.