TypeScript Types: Primitives, any vs unknown, Generics

An overview of the TypeScript type system: primitive types, inference, any vs unknown vs never, arrays and tuples, object types, unions and generics, with examples tested on TypeScript 7.0.2.

angular-typescript

TypeScript types describe which values a variable, a function parameter or a return value can hold. The compiler checks every assignment and every call against those types before the program runs, so a string passed where a number is expected becomes a compile error instead of a bug in production. The types exist only at compile time, because the compiler removes them and emits plain JavaScript.

We use types to catch wrong values early and to document what a function expects. For example, a checkout function that declares price: number rejects a price that a form passed as the text “4.50”.

The following example shows the most common type annotations, compiled with TypeScript 7.0.2 in strict mode and run on Node.js 22. Every annotation follows the same pattern, a colon after the name and the type after the colon.

// 1. Primitive types
const name: string = "Lokesh";
const age: number = 37;
const isActive: boolean = true;

// 2. Inference: no annotation needed
let city = "Delhi";                           // city: string
const country = "India";                      // country: "India"

// 3. Arrays and tuples
const ages: number[] = [37, 35, 40];
const person: [string, number] = ["Lokesh", 37];

// 4. Object type with a type alias
type User = { name: string; age: number; email?: string };
const raj: User = { name: "Raj", age: 35 };

// 5. Union and literal types
let id: string | number = 101;
let size: "small" | "large" = "small";

// 6. unknown: check before use
const input: unknown = "apple";
if (typeof input === "string") {
  const len = input.length;                   // len = 5
}

// 7. Function with typed parameters and return type
function add(a: number, b: number): number {
  return a + b;
}
const sum = add(2, 3);                        // sum = 5

// 8. Generic type
const fruits: Array<string> = ["apple", "banana"];

Notice that the lines in step 2 have no annotation, and the compiler still knows their types.

Next, we look at the parts of the type system that we use every day, starting with primitive types and type inference. After that, we cover the special types any, unknown, void and never, arrays and tuples, object types, union and literal types, function types and generics.

1. What the Type Checker Does

A type annotation fixes the type of a variable, parameter or return value. When we write let age: number, the compiler accepts only numbers for age for the rest of the program, and it reports any later assignment of a different type with the file, the line and the column.

let age: number = 37;
age = "38";

The compiler reports the second line.

src/err.ts(2,1): error TS2322: Type 'string' is not assignable to type 'number'.

Type checks happen only during compilation. The JavaScript that tsc writes to the output folder has no types left in it, so the first line of the snippet becomes let age = 37;. At runtime, a TypeScript program is a JavaScript program, so a type cannot check data that arrives later from a JSON file, a form or an HTTP response. For data from outside the program, we use runtime checks such as typeof, which section 4 shows.

TypeScript accepts JavaScript syntax, so we can rename a .js file to .ts and compile it. With strict mode on, the compiler reports some errors in a renamed file, mostly missing parameter types. That is the main practical difference between TypeScript and JavaScript for a team that migrates, and the strict option in tsconfig.json controls how many of those errors we see.

2. Primitive TypeScript Types

A primitive is a single value that is not an object, such as a piece of text or a number. TypeScript has one type for each JavaScript primitive, and the type names are written in lowercase.

TypeValuesExample
stringText in single quotes, double quotes or backticks“apple”
numberIntegers and decimals, all 64-bit floating point37, 4.5
bigintWhole numbers of any size, written with an n suffix100n
booleantrue or falsetrue
symbolUnique values, mostly used as object keysSymbol(“key”)
nullThe intentional absence of a valuenull
undefinedA value that was never setundefined

Java developers often look for int, long or double. TypeScript has only number for all of them because JavaScript stores every number as a 64-bit floating point value. For integers larger than Number.MAX_SAFE_INTEGER (9007199254740991), we use bigint.

// 1. Text and numbers
const name: string = "Lokesh";
const age: number = 37;                       // integer
const price: number = 4.5;                    // decimal, same type
const big: bigint = 9007199254740993n;        // above Number.MAX_SAFE_INTEGER

// 2. Booleans
const isActive: boolean = true;

// 3. Unique property keys
const key: symbol = Symbol("key");

// 4. Empty values
const nothing: null = null;
const notSet: undefined = undefined;

// 5. typeof at runtime
const t1 = typeof age;                        // t1 = "number"
const t2 = typeof big;                        // t2 = "bigint"
const t3 = typeof nothing;                    // t3 = "object", a JavaScript quirk

The uppercase names String, Number and Boolean also exist, but they are wrapper object types, not the primitives. Using them in annotations is a mistake, and the compiler says so.

const word: string = new String("apple");
src/err.ts(1,7): error TS2322: Type 'String' is not assignable to type 'string'.
  'string' is a primitive, but 'String' is a wrapper object. Prefer using 'string' when possible.

With strict mode on, null and undefined are separate types and cannot be assigned to a string or a number. The difference between undefined and null matters when we read optional data, such as a middle name that a user may leave empty.

3. Type Annotations and Type Inference

Type inference means the compiler works out a type from the value, so we do not have to write it. In practice, most local variables in TypeScript code have no annotation at all, and hovering over a variable in the editor shows the inferred type.

// 1. Inferred from the value
let city = "Delhi";                           // city: string
const country = "India";                      // country: "India"
const ages = [37, 35, 40];                    // ages: number[]

// 2. Inferred return type
function double(n: number) {
  return n * 2;                               // returns number
}
const result = double(4);                     // result = 8

// 3. Variable without a value needs a type
let total: number;
total = 10;

The let and const lines get different types because a let variable can change later, so the compiler widens “Delhi” to the general type string. A const variable can never change, so its type is the exact value “India”, which is called a literal type.

Function parameters are the place where inference does not help. The compiler cannot guess a parameter type from the function body, and in strict mode it refuses to fall back to any.

function double(n) {
  return n * 2;
}
src/err.ts(1,17): error TS7006: Parameter 'n' implicitly has an 'any' type.

As a rule, we annotate function parameters, exported functions and variables without a value, and we let the compiler infer the rest.

3.1. Keeping Exact Types With as const and satisfies

Two operators control how much the compiler infers from a value. A config object with a fixed set of keys, such as the prices of a small shop, is the typical case for them.

  • The as const assertion makes an array or object read-only and keeps every value as a literal type.
  • The satisfies operator, added in TypeScript 4.9, checks a value against a type but keeps the more precise inferred type.
// 1. as const: literal and readonly
const sizes = ["small", "large"] as const;    // readonly ["small", "large"]

// 2. Annotation: any string key is allowed
const stock: Record<string, number> = { apple: 5, banana: 3 };
const cherries = stock.cherry;                // cherries = undefined, no error

// 3. satisfies: value is checked, keys stay known
const prices = { apple: 2, banana: 1 } satisfies Record<string, number>;
const applePrice = prices.apple;              // applePrice = 2

With the annotation, stock accepts any key, so a typo such as stock.cherry compiles and returns undefined. With satisfies, the compiler still checks that every value is a number, but it remembers the exact keys and rejects prices.cherry.

const prices = { apple: 2, banana: 1 } satisfies Record<string, number>;
const cherryPrice = prices.cherry;
src/err.ts(2,28): error TS2339: Property 'cherry' does not exist on type '{ apple: number; banana: number; }'.

The Record type in both snippets maps every string key to a number value.

4. The Special Types: any, unknown, void and never

The types any, unknown, void and never do not describe ordinary data. Two of them, any and unknown, accept every value, whereas the other two, void and never, describe functions that return nothing.

TypeWhat can be assigned to itWhat we can do with the value
anyEvery valueEverything; the compiler checks nothing
unknownEvery valueNothing until we narrow it with a check
voidundefinedNothing useful; marks “no return value”
neverNothingNothing; marks code that never finishes normally

The any type turns off type checking for one variable. The call loose.toFixed(2) compiles when loose holds “apple”, and the program fails at runtime with a TypeError (loose.toFixed is not a function). We use any only while migrating old JavaScript code.

The unknown type is the safe replacement. It accepts any value, but the compiler does not let us use the value until a check such as typeof or instanceof proves its type (the check is called narrowing). So unknown is the right type for data from JSON.parse() or user input. For example, an app that reads a settings file with JSON.parse() must check the fields before using them, because a user can edit the file. Errors in a catch block are unknown too.

// 1. any: the compiler checks nothing
let loose: any = "apple";
const len = loose.length;                     // len = 5
loose = 42;                                   // also allowed

// 2. unknown: narrow before use
const input: unknown = "banana";
if (typeof input === "string") {
  const upper = input.toUpperCase();          // upper = "BANANA"
}

// 3. A catch variable is unknown in strict mode
try {
  const data = JSON.parse("{");               // throws SyntaxError
} catch (err) {
  const message = err instanceof Error ? err.message : String(err);
  // message = "Expected property name or '}' in JSON at position 1 (line 1 column 2)"
}

// 4. void: returns nothing useful
function log(text: string): void {
  console.log(text);
}

// 5. never: never returns normally
function fail(text: string): never {
  throw new Error(text);
}

Without the typeof check, the compiler rejects any use of the unknown value.

const input: unknown = "banana";
const upper = input.toUpperCase();
src/err.ts(2,15): error TS18046: 'input' is of type 'unknown'.

The never type has a second use. When every case of a union is handled, the remaining type is never, so an exhaustive switch over union types fails to compile when someone adds a new case. The unknown type of a catch variable is part of exception handling in TypeScript.

The diagram shows how the special types relate to the others. A value of any type can be assigned upward to unknown, and never sits below every type.

Diagram of the TypeScript type hierarchy with unknown at the top, primitive and object types in the middle, literal types below them, never at the bottom, and any outside the hierarchy
unknown accepts every value and never accepts none; any sits outside the hierarchy because it turns the checks off.

5. Arrays and Tuples

An array type describes a list where every element has the same type. There are two equivalent syntaxes.

  • The shorter form number[] is used in most code.
  • The generic form is Array<number>.

A tuple is an array with a fixed length and a fixed type at each position, which suits small groups of values such as a name and an age. For example, a CSV import reads each row as a tuple of a product name and a quantity.

// 1. Two ways to write an array type
const ages: number[] = [37, 35, 40];
const names: Array<string> = ["Lokesh", "Raj"];

// 2. Read-only array
const fruits: readonly string[] = ["apple", "banana"];

// 3. Tuple: fixed length, one type per position
const person: [string, number] = ["Lokesh", 37];
const [who, age] = person;                    // who = "Lokesh", age = 37

// 4. Named and optional tuple elements
type Point = [x: number, y: number, z?: number];
const p: Point = [1, 2];
const size = p.length;                        // size = 2

A readonly array has no methods that change it, and a tuple rejects extra elements. The compiler catches both mistakes.

const fruits: readonly string[] = ["apple", "banana"];
fruits.push("cherry");

const person: [string, number] = ["Lokesh", 37, true];
src/err.ts(2,8): error TS2339: Property 'push' does not exist on type 'readonly string[]'.
src/err.ts(4,7): error TS2322: Type '[string, number, boolean]' is not assignable to type '[string, number]'.
  Source has 3 element(s) but target allows only 2.

TypeScript arrays have many more methods, and an array of objects uses an object type for its elements.

6. Object Types, Type Aliases and Interfaces

An object type lists the property names of an object and the type of each property. We give it a name with a type alias (the type keyword) or with an interface. A ? after a property name makes it optional, and readonly prevents changes after the object is created.

// 1. Type alias for an object
type User = {
  readonly id: number;                        // cannot change later
  name: string;
  age: number;
  email?: string;                             // optional
};
const raj: User = { id: 1, name: "Raj", age: 35 };

// 2. Interface with the same shape
interface Fruit {
  name: string;
  count: number;
}
const apple: Fruit = { name: "apple", count: 5 };

// 3. Shape matters, not the name
const john = { name: "John", age: 40, city: "Pune" };
const named: { name: string } = john;         // OK, extra property allowed

const email = raj.email;                      // email = undefined

The third example shows a rule that confuses Java developers. The john object was never declared as { name: string }, but it has a name string, so the assignment compiles. TypeScript uses structural typing, so a value fits a type when it has the required properties, whatever its declared name is.

There is one exception. When we write an object literal in the place where a type is expected, the compiler rejects unknown properties, because a misspelled property name is a common bug. It also rejects changes to readonly properties.

type User = { readonly id: number; name: string; age: number };
const john: User = { id: 3, name: "John", age: 40, city: "Pune" };
john.id = 4;
src/err.ts(2,52): error TS2353: Object literal may only specify known properties, and 'city' does not exist in type 'User'.
src/err.ts(3,6): error TS2540: Cannot assign to 'id' because it is a read-only property.

For object shapes, a type alias and an interface work almost the same. An interface can be extended with extends and merged across declarations, whereas a type alias can also name unions, tuples and primitives. Many teams use interfaces for object shapes, such as the objects created from an interface in a service layer, and type aliases for everything else.

7. Union, Literal and Intersection Types

TypeScript can build new types from existing ones. For example, an order status that is either “new” or “shipped” is a union of two literal types.

  • A union type (A | B) accepts a value of either type.
  • A literal type accepts one exact value, and a union of literals works like a short list of allowed values.
  • An intersection type (A & B) requires all properties of both types.
// 1. Union: one of several types
let id: string | number = 101;
id = "101";

// 2. Literal types: exact values
type Size = "small" | "medium" | "large";
const size: Size = "medium";

// 3. Intersection: all properties of both
type Named = { name: string };
type Aged = { age: number };
const lokesh: Named & Aged = { name: "Lokesh", age: 37 };

A union of string literals is the common TypeScript replacement for a small enum. Unions also support narrowing and discriminated unions, and literal types work well with as const. Many projects avoid enums in favor of literal unions.

8. Function Types

A function signature has a type for each parameter and one for the return value. The return type can be inferred, but writing it on exported functions makes mistakes inside the function body show up at the function itself rather than at every caller. A function type, written with an arrow, describes a function stored in a variable or passed as an argument.

// 1. Parameter and return types
function add(a: number, b: number): number {
  return a + b;
}

// 2. Optional and default parameters
function greet(name: string, greeting = "Hello", suffix?: string): string {
  return greeting + ", " + name + (suffix ?? "");
}

// 3. A function type
type Compare = (a: number, b: number) => number;
const byValue: Compare = (a, b) => a - b;

const sum = add(2, 3);                        // sum = 5
const text = greet("Raj");                    // text = "Hello, Raj"
const sorted = [40, 35, 37].sort(byValue);    // sorted = [35, 37, 40]

In byValue, the parameters a and b need no annotation, because the Compare type already gives them the type number. Functions can also take optional and default parameters or rest parameters, and function overloading gives one function several signatures.

9. Generic Types

A generic type has a type parameter, written in angle brackets, that the caller fills in. The type Array<string> is the built-in example, where Array is generic and string is its type argument. Generics let us write one function or type that works for many element types and still keeps the exact type of each call. For example, one first() helper returns the first product of a product list and the first user of a user list, each with the right type.

// 1. Generic function
function first<T>(items: T[]): T | undefined {
  return items[0];
}
const age = first([37, 35]);                  // age = 37, type number | undefined
const fruit = first(["apple", "banana"]);     // fruit = "apple"

// 2. Generic type alias
type Box<T> = { value: T };
const box: Box<number> = { value: 5 };

The compiler infers T from the argument, so first([37, 35]) returns number | undefined and first([“apple”, “banana”]) returns string | undefined. Generics also work with classes and interfaces, and constraints limit the allowed type arguments.

10. Running the Type Examples

The example project on GitHub contains every compiling snippet, one file per section, and an index.ts that runs them in order. It uses TypeScript 7.0.2 and Node.js 22 or newer.

npm install
npm start

The npm start script compiles the project with strict mode and prints the values shown in the snippet comments. The examples marked “Code that does not compile” are not in the project, because they would stop the build.

11. Conclusion

TypeScript types are compile-time checks on top of JavaScript. They range from primitives such as string and number to object types named with type or interface, and to combinations built with unions, literals and generics.

The compiler infers most types, so we annotate parameters and exported functions and leave the rest to inference. For values of unknown shape, unknown plus a runtime check is the safe choice, and any is a last resort.

12. References

The TypeScript Handbook is the primary reference for the type system, and MDN documents the JavaScript values behind each type.

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.