Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. Compare
  3. JavaScript vs TypeScript
  4. JavaScript to TypeScript Variables

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

JavaScript
let count = 0;
const name = "Ada";
let items = [];
TypeScript
let count: number = 0;  // explicit (optional)
const name = "Ada";    // infers string
let items: string[] = [];  // needed here

Object with type

JavaScript
const user = {
  name: "Ada",
  age: 30,
};
TypeScript
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

JavaScript
const config = {
  mode: "dark",
  retries: 3,
};
// typeof config.mode is just "string"
TypeScript
const config = {
  mode: "dark",
  retries: 3,
} as const;
// typeof config.mode is "dark"
// typeof config.retries is 3

Array as const

JavaScript
const roles = ["admin", "user", "guest"];
// roles[0] type is string
TypeScript
const 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

JavaScript
function double(n) {
  return n * 2;
}
// no way to know return type statically
TypeScript
function double(n: number) {
  return n * 2;
}
// return type inferred as number

Destructuring inherits types

JavaScript
const { name, age } = getUser();
// name and age are untyped
TypeScript
const { 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

JavaScript
let name = "Ada";
let age = 30;
let active = true;
let scores = [95, 87, 91];
TypeScript
let name: string = "Ada";
let age: number = 30;
let active: boolean = true;
let scores: number[] = [95, 87, 91];

Readonly array

JavaScript
function first(items) {
  // nothing stops mutation
  items.push("extra");
  return items[0];
}
TypeScript
function first(items: readonly string[]) {
  // items.push("extra"); // compile error
  return items[0];
}
Warm-up1 / 2

Can you write this from memory?

Check if string `item_text` matches the pattern `/abc/`.

JavaScript: JavaScript String Methods Practice →

Practice Both Languages

10 free exercises a day. No credit card required. Build syntax muscle memory with spaced repetition.

Free forever. No credit card required.

← Back to JavaScript vs TypeScript
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.