skip to content

Basic Types & Annotations

The type vocabulary you reach for in every TypeScript file: primitives, literals, arrays and tuples, the special types (any/unknown/never/void), enums and their modern alternatives, and how you annotate functions. Interviewers open here because sloppy answers about any vs unknown or enums reveal exactly how much of the type system a candidate actually reasons about.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

explore

questions

page 1 of 2

In TypeScript, is `string[]` a different type from `Array<string>`, and which of the two forms can you write `readonly` in front of?

level: juniorimportance: must knowfreq 72%

answer

  1. one type, two spellings
  2. brackets are sugar for the generic
  3. the modifier keyword needs somewhere to attach
  4. readonly only on the bracket form
  5. ReadonlyArray<T> is the generic spelling

basics

~20 s

string[] and Array<string> are the same type — the bracket form is shorthand for the generic one. They differ only in syntax: readonly attaches to the bracket form (readonly string[]); the generic spelling is ReadonlyArray<string>.

solid answer

~40 s

They are the same type. `string[]` is sugar for `Array<string>`, the checker treats them as one type, and inference never distinguishes them, so you can assign either to the other freely. Choosing between them is style: the bracket form is terser for simple element types, while the generic form reads better for a long or union element type, since `Array<string | number>` needs no parentheses where `(string | number)[]` does. The one place they stop being interchangeable is the `readonly` modifier: `readonly string[]` is legal, but `readonly Array<string>` is a syntax error, because that modifier is only permitted on array and tuple literal type syntax — the generic spelling of that type is `ReadonlyArray<string>`. Nesting works in both forms: `string[][]` is `Array<Array<string>>`. Most teams pick one form and enforce it with a lint rule.

go deeper

for a junior

Be ready to say plainly that string[] and Array<string> are the same type and that the bracket form is shorthand. Knowing that readonly attaches only to the bracket form is the detail that lifts the answer above a shrug.

for a middle

Explain the mechanics: the bracket syntax expands to the Array interface, so inference, error text and assignability are identical, and the readonly modifier is a syntax restriction rather than a semantic difference between the forms.

for a senior

Show that you treat this as a codebase-consistency decision — one form chosen and lint-enforced — and point out the concrete consequence that a team on Array<T> must still reach for ReadonlyArray<T> when it wants immutable parameters.

for a principal

Frame it as a convention you standardise once and never relitigate, and be able to say why: the readability of long element types versus the readonly ergonomics, with the migration cost of flipping an established codebase weighed against zero semantic gain.

## Two spellings, one type TypeScript offers two syntaxes for "array of T". The bracket form writes the element type followed by `[]`, and the generic form instantiates the built-in `Array` interface: ```typescript const a: string[] = ['x']; const b: Array<string> = a; // same type, assignment is fine const c: string[] = b; // also fine ``` These are not two similar types that happen to be compatible — they are one type with two spellings. The bracket syntax is expanded by the compiler into `Array<T>`, so error messages, `keyof`, method lookup and inference all behave identically. Nothing about the choice reaches the emitted JavaScript either: types are erased at compile time, so both forms produce the same output. ## Nesting and element-type readability Both forms nest. `string[][]` is an array of arrays of strings, and its generic spelling is `Array<Array<string>>`. As the element type grows, the bracket form starts to strain: ```typescript type UserList = { id: string; name: string }[]; // legal, but the [] is easy to miss type UserList2 = Array<{ id: string; name: string }>; // the element type is bracketed for you ``` The practical argument for the generic form is that it puts the element type inside visible delimiters. The practical argument for the bracket form is brevity for short element types — `number[]` beats `Array<number>` for scanning. Neither is more correct; what matters is consistency, which teams usually settle with a lint rule rather than a per-file decision. ## Union element types force a decision The bracket form binds more loosely than a union, so an element union must be parenthesised: ```typescript type Mixed = (string | number)[]; // array whose elements are string or number type Mixed2 = Array<string | number>; // identical, no parentheses required ``` Without the parentheses you get a different type entirely, which is the single most common beginner mistake with the bracket syntax. ## Where the forms genuinely diverge: readonly The `readonly` type modifier is only permitted on array and tuple literal type syntax. So: ```typescript type Ok = readonly string[]; type Ok2 = ReadonlyArray<string>; // the generic spelling of the same type // type Bad = readonly Array<string>; // syntax error: readonly is not allowed here ``` `readonly string[]` and `ReadonlyArray<string>` denote the same type — an array-like type that carries the reading methods (`map`, `filter`, `slice`, `concat`, `indexOf`, `at`, …) but not the mutating ones (`push`, `pop`, `splice`, `sort`, `reverse`, `fill`), and whose index signature is readonly. If you have standardised on `Array<T>` everywhere, you still have to reach for `ReadonlyArray<T>` when you want immutability, because the modifier keyword has no place to attach in the generic form. Teams that standardise on the bracket form get `readonly T[]` for free, which is one honest reason to prefer it. When the element type is itself a union, both parts stack: `readonly (string | number)[]`, or `ReadonlyArray<string | number>`. ## What is not different A few things people expect to differ and do not: - **Inference.** An array literal is inferred as `T[]` in the bracket spelling in tooling output, but that is a display choice; the type is the same one `Array<T>` names. - **Mutability.** Neither form is more mutable than the other. `Array<string>` is not somehow "the mutable one" — `string[]` is equally mutable. - **Runtime.** Both erase completely. Neither adds a check, a wrapper, or any cost. - **Assignability.** Because they are the same type, there is no conversion, no widening, and no structural comparison to reason about between them. The takeaway an interviewer wants: you know these are one type, you can articulate the readonly asymmetry as the only hard difference, and you treat the rest as a style question your team settles once.

  • If the two forms are identical, is there any case where you cannot use the bracket form?
    Not for expressing the type itself — anything `Array<T>` can express, `T[]` can, given parentheses for unions and intersections. The pressure runs the other way: `readonly` attaches only to the bracket form, so a codebase standardised on `Array<T>` has to switch to `ReadonlyArray<T>` for immutable arrays. Beyond that it is legibility, not capability.
  • Does the choice of form change what TypeScript emits?
    No. Type annotations are erased entirely during compilation, so both forms emit the same JavaScript — in fact no JavaScript at all for the annotation itself. The distinction lives only in the checker, which is why the decision is purely about how the source reads to a human.
  • How would you write an array of arrays of numbers in both forms?
    `number[][]` in the bracket form and `Array<Array<number>>` in the generic form. Note that `readonly number[][]` makes only the outer array immutable — the inner arrays are still fully mutable. Making both levels readonly requires `readonly (readonly number[])[]`, which is where the bracket form starts to lose its readability advantage.

saying these in an interview costs you the question

  • Claims Array<T> is mutable and T[] is readonly
  • Says the two forms produce different JavaScript
  • Writes readonly Array<string> and expects it to compile
  • Thinks one form infers differently from the other
  • Believes Array<T> adds a runtime wrapper or cost

context

open as a page

In TypeScript, what does the annotation `[string, number]` guarantee about a value that the annotation `(string | number)[]` does not?

level: juniorimportance: must knowfreq 68%

basics

~20 s

A tuple type fixes both the length and the type of each position: [string, number] accepts exactly two elements, a string then a number. (string | number)[] accepts any number of elements, in any order.

open as a page

In TypeScript, given `enum Direction { Up = 1, Down, Left, Right }`, what value does each member hold — and why does `enum Level { Low = 'LOW', High }` fail to compile?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Numeric enum members auto-increment from the previous value, so Up = 1 makes Down 2, Left 3 and Right 4. String members never auto-increment, so any member following a string member must have its own explicit initializer.

open as a page

In TypeScript, what does the question mark do in `function log(msg: string, level?: string)`, and what rules govern where such a parameter may appear?

level: juniorimportance: must knowfreq 70%

basics

~20 s

A question mark makes the parameter optional: callers may omit it, and inside the body its type includes undefined. Optional parameters must follow all required ones, and a parameter cannot have both a question mark and a default value.

open as a page

In TypeScript, the function `function add(a, b) { return a + b; }` is rejected under the `noImplicitAny` compiler option, yet the same function needs no return type annotation at all. Why does the compiler treat parameters and return types so differently?

level: juniorimportance: must knowfreq 82%

basics

~20 s

TypeScript infers a return type from the function body, but nothing tells it what callers will pass, so unannotated parameters fall back to the implicit any type, which noImplicitAny rejects. Annotate the inputs; let the body infer the output.

open as a page

In TypeScript, what is the difference between annotating a value with the lowercase type `string` and the capitalised type `String`, and which one should you write?

level: juniorimportance: must knowfreq 72%

basics

~20 s

Lowercase string is the primitive type; capitalised String is the standard-library interface describing the boxed wrapper object. A string is assignable to String, but a String is not assignable to string, so always annotate with the lowercase primitives.

open as a page

In TypeScript, what type is inferred for `const roles = ['admin', 'user']`, and how does adding `as const` to that array literal change it?

level: juniorimportance: must knowfreq 72%

basics

~10 s

TypeScript infers string[] there: a mutable array of widened strings. Adding as const infers readonly ["admin", "user"] instead — a readonly tuple whose elements keep their exact literal types, with fixed length and position.

open as a page

In TypeScript, what is a string literal type such as 'GET', and what does the union type 'GET' | 'POST' | 'DELETE' allow as a value?

level: juniorimportance: must knowfreq 80%

basics

~20 s

A literal type's domain is a single value: the type 'GET' accepts only the string 'GET'. A union such as 'GET' | 'POST' | 'DELETE' accepts exactly those three strings and rejects every other string.

open as a page

In TypeScript, what is the difference between the `any` and `unknown` types, and which one would you give to a value returned by JSON.parse?

level: juniorimportance: must knowfreq 88%

basics

~20 s

any turns off type checking for a value and everything derived from it; unknown accepts any value but permits almost no operations until you narrow or assert it. Use unknown for JSON.parse output, then validate before use.

open as a page

In TypeScript, what changes when the strictNullChecks compiler option is turned on, and what must code do differently before it can use a value that might be null or undefined?

level: juniorimportance: must knowfreq 78%

basics

~20 s

strictNullChecks gives null and undefined their own types instead of letting them inhabit every other type. A value typed string | undefined must then be narrowed with a check, optional chaining, or a default before it can be used as a string.

open as a page

In TypeScript, what types are inferred for `const mixed = [1, 'a']` and `const tags = ['a', 'b']`, and why is `tags` not inferred as `('a' | 'b')[]`?

level: middleimportance: must knowfreq 62%

basics

~20 s

mixed is inferred as (string | number)[] and tags as string[]. TypeScript takes the union of the element types and widens literal types, because a mutable array's slots can later be reassigned to any other string.

open as a page

In TypeScript, a `number[]` can be assigned to a variable of type `readonly number[]`, but not the other way round. Why does assignability only flow in that direction?

level: middleimportance: must knowfreq 58%

basics

~20 s

readonly number[] has the reading methods but not the mutating ones, so every number[] already satisfies it. The reverse assignment would hand push and sort to a value whose owner is relying on it not being mutated, so the compiler rejects it.

open as a page

In TypeScript, what JavaScript does the compiler emit for `enum Direction { Up, Down }` compared with `enum Dir { Up = 'UP', Down = 'DOWN' }`, and why can you look a member's name up by its value only in the first case?

level: middleimportance: must knowfreq 60%

basics

~20 s

Both emit a var plus a self-invoking function that fills an object. Numeric members are written in both directions — name to value and value back to name — so Direction[0] is 'Up'. String members get only the forward entry, so string enums have no reverse lookup.

open as a page

In TypeScript, what does an `enum` declaration emit into the compiled JavaScript that a union of string literals does not, and why does that difference matter?

level: middleimportance: must knowfreq 62%

basics

~20 s

An enum is one of the few TypeScript constructs that emits runtime code: a real object built by a self-invoking function. A union of string literals emits nothing, so it costs no bytes and works in tools that only strip types.

open as a page

In TypeScript, what type does the compiler infer for `const n = 42` compared with `let n = 42`, and why do the two differ?

level: middleimportance: must knowfreq 66%

basics

~20 s

TypeScript infers the narrow type 42 for const n = 42 and the wider type number for let n = 42. A const can never be reassigned, so the narrow type stays accurate; a let can be, so the compiler widens it to the primitive.

open as a page

In TypeScript, given `const STATUS = { idle: 'idle', busy: 'busy' };`, how do you derive a union type of its values, and what has to be true of the object for that to work?

level: middleimportance: must knowfreq 60%

basics

~20 s

Assert the object with as const so its values keep their literal types, then take an indexed access over its keys: type Status = (typeof STATUS)[keyof typeof STATUS], which is "idle" | "busy". Without as const the values widen and the union collapses to string.

open as a page

This TypeScript code fails to compile: `declare function request(url: string, method: 'GET' | 'POST'): void; const config = { method: 'GET' }; request('/users', config.method);`. What type did the compiler infer for config.method, why, and how would you fix it?

level: middleimportance: must knowfreq 68%

basics

~20 s

The compiler inferred string for config.method, because an object literal's property is a mutable location and its fresh literal type widens. Fix it by annotating the object with the union, adding as const, or passing 'GET' inline.

open as a page

A value typed `any` in TypeScript is often called contagious. What exactly happens to the types of expressions derived from it, and which checks does the compiler stop performing?

level: middleimportance: must knowfreq 66%

basics

~20 s

A value typed any makes every expression derived from it any as well — property reads, calls, indexing, awaits — so whole branches of code stop being checked, and the failure surfaces at runtime far from the original annotation.

open as a page

In TypeScript, what return type is inferred for a function that always throws — and why does `function fail(m: string) { throw new Error(m); }` infer a different type from `const fail = (m: string) => { throw new Error(m); };`?

level: middleimportance: must knowfreq 55%

basics

~20 s

A function expression or arrow function whose body always throws or loops forever infers never. A function declaration with the same body infers void instead, so you must annotate it : never explicitly to get the never return type.

open as a page

Given `enum Priority { Low = 1, High = 2 }` in TypeScript, does `const p: Priority = value` compile when `value` is typed `number`, and what does the answer mean for enum-typed data that arrived as JSON?

level: seniorimportance: must knowfreq 48%

basics

~20 s

Yes, it compiles. TypeScript still lets any value of type number flow into a numeric enum type, so a number parsed from JSON is accepted unchecked. Only an out-of-range numeric literal is rejected, so validate at the boundary.

open as a page

In TypeScript, why is a function that takes fewer parameters assignable where one taking more is expected, and what bug can that allow through?

level: seniorimportance: must knowfreq 52%

basics

~20 s

A function that ignores trailing parameters can safely stand in for one that receives them, so TypeScript allows fewer parameters but never more. The risk is that a callback silently receives extra arguments it does not expect, which then land in a parameter that has a default.

open as a page

In TypeScript, when is it worth writing an explicit return type annotation on a function, and when is letting the compiler infer it the better choice?

level: seniorimportance: must knowfreq 62%

basics

~20 s

Annotate the return type when the result should be checked against a contract you chose: exported API, recursive functions, and anywhere a wrong result must be reported at the definition rather than at distant call sites. Let local helpers and callbacks infer.

open as a page

In TypeScript, what do the annotations `string | number[]` and `(string | number)[]` each describe, and how would you write the second one using the `Array<...>` form?

level: juniorimportance: should knowfreq 48%

basics

~20 s

string | number[] is a union of a string and an array of numbers, because [] binds tighter than |. (string | number)[] is an array whose elements may each be a string or a number, written generically as Array<string | number>.

open as a page

In TypeScript, what does the type `never` represent, and what can you assign to a variable declared `let x: never`?

level: juniorimportance: should knowfreq 50%

basics

~20 s

never is TypeScript's bottom type: no value has it, so nothing can be assigned to a never variable, while a never value is assignable to every other type. It marks code that cannot be reached.

open as a page

In the TypeScript tuple type `[string, number?, ...boolean[]]`, what do the `?` and the `...` mean, and what is that tuple's `length` type?

level: middleimportance: should knowfreq 42%

basics

~20 s

The ? marks an optional element that may be omitted, and reading it yields number | undefined. The ... is a rest element allowing zero or more booleans at the end. Because the rest element makes the size open-ended, length is typed number.

open as a page

In TypeScript, a function `move(x: number, y: number)` is called as `move(...args)`. Why does that fail to compile when `args` is `number[]`, and what type of `args` makes it work?

level: middleimportance: should knowfreq 40%

basics

~20 s

A number[] has unknown length, so the compiler cannot prove the call supplies exactly two arguments and reports that a spread argument must have a tuple type or be passed to a rest parameter. Typing args as the tuple [number, number] fixes the arity and the call compiles.

open as a page

What changes in the emitted JavaScript when you declare a TypeScript enum as `const enum LogLevel { Debug, Info, Warn }` instead of `enum LogLevel { ... }`, and what can you no longer do with it?

level: middleimportance: should knowfreq 48%

basics

~20 s

A const enum emits no object at all: every member reference is replaced by its literal value at the call site, so LogLevel.Warn becomes 2. In exchange you lose the runtime object — no reverse lookup, no iteration, and the enum name is only legal in property or index access positions.

open as a page

In TypeScript, `enum Status { Active = 'active' }` rejects `const s: Status = 'active'`, while `type Status = 'active' | 'archived'` accepts it. What rule explains the difference, and why does comparing members of two different enums raise an error?

level: middleimportance: should knowfreq 52%

basics

~20 s

An enum declaration creates its own distinct type whose members are identified by where they were declared, not by their value, so a matching string literal is not assignable to it. A literal union is structural: any value of the right shape belongs.

open as a page

In TypeScript, what does giving a parameter a default value do to the function's type — what is inferred for `mode` in `function run(mode = 'fast') {}`, and is the parameter optional?

level: middleimportance: should knowfreq 58%

basics

~20 s

A default makes the parameter omittable and lets the compiler infer its type from the initializer. mode is inferred as string, not the literal 'fast', because inference widens literals in mutable positions. Inside the body the type excludes undefined.

open as a page

In TypeScript, what is the difference between declaring a function parameter as `x?: number` and declaring it as `x: number | undefined`?

level: middleimportance: should knowfreq 55%

basics

~20 s

Both allow the value undefined, but only x?: number lets the caller omit the argument. With x: number | undefined the argument is still required, so the caller must write undefined explicitly. Inside the body both have type number | undefined.

open as a page

showing 1–30 of 56