One of TypeScript's main strengths is its rich type system. Understanding the basic types is essential for writing effective TypeScript code. This guide covers the fundamental types available in TypeScript and how to use them in your projects.
TypeScript also supports special numeric values like NaN (not a number) and Infinity:
NaN / Infinity
let notANumber:number=NaN;let infiniteValue:number=Infinity;
String
Text data is represented using the string type. You can use single quotes ('), double quotes ("), or template literals (`) to create string values.
string
let firstName:string="John";let lastName:string="Doe";// Template literals can span multiple lines and embed expressionslet fullName:string=`${firstName}${lastName}`;let greeting:string=`Hello, ${fullName}!Welcome to TypeScript.`;
Symbol
Symbols are immutable and unique. They were introduced in ECMAScript 2015 and can be used as keys for object properties.
symbol
let sym1:symbol=Symbol();let sym2:symbol=Symbol("key"); // optional string key// Each symbol is uniquelet areEqual:boolean= sym1 === sym2; // always false
BigInt
BigInt represents whole numbers larger than 2^53 - 1. They are created by appending n to the end of an integer or by calling the BigInt() function.
bigint
// Only available when targeting ES2020 or laterlet bigNumber:bigint=9007199254740991n;let anotherBigNumber:bigint=BigInt(9007199254740991);
TypeScript has two special types, null and undefined, which represent different concepts:
null: Explicitly indicates the absence of a value or that a value is intentionally empty. It represents a deliberate "nothing" value and must be assigned.
undefined: Represents a value that hasn't been assigned yet or doesn't exist. Variables are automatically undefined when declared but not initialized.
These differences reflect how JavaScript treats these values:
null / undefined
// Variables explicitly set to nulllet userContact:string|null=null; // We know there's no contact infolet selectedItem:object|null=null; // No item is selected// Variables that are undefinedlet userName:string; // Declared but not initialized, so it's undefinedconsole.log(userName); // undefinedfunctionfindUser(id:number):object|undefined {// Return undefined when no user is foundif (id <0) returnundefined;// ...}// The difference in checkinglet value1 =null;let value2 =undefined;console.log(typeof value1); // "object" (historical JavaScript quirk)console.log(typeof value2); // "undefined"
By default, both null and undefined are subtypes of all other types, meaning you can assign them to something like number:
subtypes
let n:null=null;let u:undefined=undefined;// This is allowed by default (unless strictNullChecks is enabled)let num:number=null;let id:number=undefined;
When using the --strictNullChecks flag (recommended), null and undefined are only assignable to unknown, any, and their respective types. This helps avoid many common errors:
--strictNullChecks
// With strictNullCheckslet num:number=null; // Errorlet id:number=undefined; // Errorlet maybeNum:number|null=null; // OK with union typelet maybeId:number|undefined=undefined; // OK with union type
any
The any type allows you to opt-out of type checking and essentially revert back to JavaScript's dynamic typing:
any
let notSure:any=4;notSure ="maybe a string";notSure =false; // okay, definitely a boolean// Using any disables type checkingnotSure.toFixed(); // No compile error, might fail at runtimenotSure.someNonExistentMethod(); // No error either!
Important: The any type is generally frowned upon in TypeScript and should be avoided whenever possible.
Using any effectively undermines the entire purpose of TypeScript by:
Allowing any operation without type checking
Bypassing compiler safeguards
Eliminating IDE assistance and autocomplete
Potentially introducing runtime errors
Making code harder to refactor and maintain
Instead of using any, consider these alternatives:
Use unknown for values of uncertain types (then narrow them with type guards)
Define proper interfaces or types for your data
Use union types to represent multiple possible types
Use generics for flexible but type-safe code
There are very few legitimate use cases for any:
During incremental adoption of TypeScript in an existing JavaScript project
Working with third-party libraries without type definitions
Rare edge cases where the type system cannot express a particular pattern
Even in these cases, try to limit the scope of any as much as possible.
unknown
The unknown type is TypeScript's type-safe counterpart to any. While both can hold values of any type, they behave very differently in how they let you interact with those values.
unkown
let valueAny:any=10;let valueUnknown:unknown=10;// With any, you can do anything - no type safety!valueAny.toFixed(2); // No errorvalueAny.someNonExistentMethod(); // No errorvalueAny.foo.bar.baz; // No errorlet num1:number= valueAny; // No error// With unknown, you can't do anything without type checking// valueUnknown.toFixed(2); // Error: Object is of type 'unknown'// valueUnknown.length; // Error: Object is of type 'unknown'// let num2: number = valueUnknown; // Error: Type 'unknown' is not assignable to type 'number'
Key differences between any and unknown:
Type safety:
any: Completely bypasses type checking
unknown: Enforces type checking before operations
Assignment compatibility:
any: Can be assigned to any other type
unknown: Can only be assigned to any or unknown types
Operations allowed:
any: All operations allowed without checks
unknown: No operations allowed until type is narrowed
Property access:
any: Any property can be accessed
unknown: No properties can be accessed without type narrowing
To use an unknown value, you must first narrow its type using type guards:
Use of unknown
let value:unknown="Hello, TypeScript!";// Using type guards to narrow the typeif (typeof value ==="string") {console.log(value.toUpperCase()); // OK: now treated as string}// Alternative using type assertion (use with caution)console.log((value asstring).toUpperCase());// Using instanceof for objectslet someDate:unknown=newDate();if (someDate instanceofDate) {console.log(someDate.toISOString()); // OK: now treated as Date}
unknown is the recommended approach when you need to represent a value whose type you don't know yet, as it forces you to perform proper type checking before performing operations, preventing runtime errors.
void
The void type represents the absence of a value, commonly used as the return type of functions that don't return a value.
void
functionlogMessage(message:string):void {console.log(message);// No return statement or returns undefined}// Variable of type void can only be assigned undefined (or null if strictNullChecks is disabled)let unusable:void=undefined;
void in Arrow Functions and Callbacks
The void type is especially important in TypeScript when working with callback functions, particularly in React component props and event handlers:
void callback functions
// Arrow function that returns voidconstlogError= (error:Error):void=> {console.error(error.message);};// Callback function type with void returntypeClickHandler= (event:MouseEvent) =>void;// Using void in function type definitionsinterfaceButtonProps {onClick: () =>void;onHover?: (id:string) =>void;}
Why void is Essential in React Props
In React with TypeScript, the void return type is crucial for event handlers and callback props:
void propping
// Component with callback propsinterfaceSubmitButtonProps {// void return type means the parent doesn't expect a return valueonSubmit: () =>void;onCancel: () =>void;}constSubmitButton= ({ onSubmit, onCancel }:SubmitButtonProps) => {return ( <div><button onClick={onSubmit}>Submit</button><button onClick={onCancel}>Cancel</button></div> );};// Using the componentconstForm= () => {// These handlers don't need to return anythingconsthandleSubmit= () => {console.log('Submitted');// Implementation details... };consthandleCancel= () => {console.log('Cancelled');// Implementation details... };return (<SubmitButton onSubmit={handleSubmit} onCancel={handleCancel}/> );};
Important Distinction with void
A key point about the void type in TypeScript:
A function declared with a void return type can return any value, but the return value will be ignored:
return value ignored
functionwarnUser():void {console.log("Warning!");returntrue; // Valid, but the return value is ignored}constresult=warnUser(); // result is of type void, not boolean
But when a function type has a void return type, it signals intent that the function's return value should not be used, even if it returns something:
return value not being used
typeVoidCallback= () =>void;constcallback:VoidCallback= () => {return"hello"; // Allowed despite void return type};// The return is allowed, but you're not supposed to use itconstvalue=callback(); // value is of type void, not string
This behavior is intentional in TypeScript to enable callbacks to be assigned to void-returning function types even if they happen to return values.
never
The never type represents values that never occur. It's used for functions that never return (because they throw exceptions, have infinite loops, or always terminate the program) and for variables that can never have a value due to type narrowing.
never
// Function that never returnsfunctionerror(message:string):never {thrownewError(message);}// Function that always throwsfunctionfail():never {returnerror("Something failed");}// Function with an infinite loopfunctioninfiniteLoop():never {while (true) {// code that never exits }}
How never differs from any and unknown
The never type has important differences from any and unknown:
Relationship in the type hierarchy:
any: Top type - can be assigned to and from any other type
unknown: Top type - can be assigned from any type, but not to other types without narrowing
never: Bottom type - can be assigned to any other type, but no type can be assigned to never
Representing impossibility:
any: Represents "could be anything, we don't care about the type"
unknown: Represents "could be anything, but we need to check before using it"
never: Represents "this cannot/will not happen"
Usage in exhaustive checking:
One powerful use of never is in exhaustive checks with discriminated unions:
exhaustive check
typeShape=Circle|Square|Triangle;interfaceCircle { kind:"circle"; radius:number;}interfaceSquare { kind:"square"; sideLength:number;}interfaceTriangle { kind:"triangle"; base:number; height:number;}functiongetArea(shape:Shape):number {switch (shape.kind) {case"circle":returnMath.PI*shape.radius **2;case"square":returnshape.sideLength **2;case"triangle":return (shape.base *shape.height) /2;default:// This line ensures all cases are covered// If we add a new shape type but forget to handle it,// TypeScript will give us an error hereconstexhaustiveCheck:never= shape;return exhaustiveCheck; }}
When used this way, if we later add a new type to the Shape union but forget to handle it in the switch statement, TypeScript will throw an error because the unhandled case would fall through to the default, and the new shape type can't be assigned to never.
Return types:
any: A function returning any could return any value
unknown: A function returning unknown could return any value, but the caller must verify the type
never: A function returning never doesn't return normally at all (throws, infinite loop, etc.)
The never type is especially useful in type systems to represent impossible states and to create more type-safe code by ensuring exhaustive handling of all possible cases.
// Using the type followed by []let list1:number[] = [1,2,3];// Using the generic Array typelet list2:Array<number> = [1,2,3];// Array of mixed types with a union typelet mixed: (number|string)[] = [1,"two",3];// Type inference works with arrays toolet inferred = [1,2,3]; // inferred as number[]
Arrays in TypeScript are just like JavaScript arrays with added type safety. You can use all the familiar array methods.
Array methods
let numbers:number[] = [1,2,3,4,5];numbers.push(6); // OK// numbers.push("7"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'let firstItem = numbers[0]; // Type is numberlet moreNumbers =numbers.map((n) => n *2); // Still number[]
Tuples
Tuples are a special type of array where the type of a fixed number of elements is known. The types don't have to be the same.
Tuples
// Declare a tuple typelet person: [string,number] = ["John",25];// Access with correct typeslet name:string= person[0];let age:number= person[1];// Error when accessing with incorrect type// let error: string = person[1]; // Error: Type 'number' is not assignable to type 'string'// Error when adding more elements than defined// person[2] = "extra"; // Error in strict mode
When accessing an element with a known index, the correct type is retrieved. Tuples are particularly useful when you want to represent a fixed structure, like a key-value pair or a row in a CSV file.
// Representing a CSV row [id, name, active]typeCSVRow= [number,string,boolean];let rows:CSVRow[] = [ [1,"Alice",true], [2,"Bob",false],// [3, "Charlie"] // Error: Type '[number, string]' is not assignable to type 'CSVRow'];
Recent versions of TypeScript also support named tuple elements for better documentation:
Enums significantly enhance code maintainability by providing meaningful, self-documenting constants and improving type safety. Consider this comparison:
Without enums (using string literals):
Without enums
// Using string literals directlyfunctionprocessResponse(status:string) {if (status ==="success") {// Process successful response } elseif (status ==="error") {// Handle error } elseif (status ==="pending") {// Handle pending state }// What if someone passes "SUCCESS" or "Success" instead?}// Calling the functionprocessResponse("success");processResponse("SUCCESS"); // No error, but might not work as expectedprocessResponse("pending");processResponse("wating"); // Typo, but TypeScript won't catch this!
With enums:
With enums
enumResponseStatus { Success ="success", Error ="error", Pending ="pending",}functionprocessResponse(status:ResponseStatus) {switch (status) {caseResponseStatus.Success:// Process successful responsebreak;caseResponseStatus.Error:// Handle errorbreak;caseResponseStatus.Pending:// Handle pending statebreak; }}// Calling the functionprocessResponse(ResponseStatus.Success); // CorrectprocessResponse(ResponseStatus.Pending); // Correct// processResponse("success"); // Error: Argument of type '"success"' is not assignable to parameter of type 'ResponseStatus'// processResponse("wating"); // Error: Argument of type '"wating"' is not assignable to parameter of type 'ResponseStatus'
Key Benefits of Enums for Equality Checks
Type Safety: TypeScript will ensure you only use valid enum values.
Autocomplete: Your IDE will suggest the available enum values.
Centralized Definition: If you need to add, remove, or rename a status, you only change it in one place.
Refactoring Support: If you rename an enum value, TypeScript will help you find all usages.
Self-Documenting: The code clearly indicates what values are valid and expected.
Runtime Structure: Enums exist at runtime as objects, allowing you to iterate over them if needed.
In this example, using an enum for request status makes the code more maintainable, self-documenting, and less prone to typos or inconsistencies compared to using string literals directly.
Objects
Object types can be defined using interfaces or type aliases. Here's an example using inline object type annotation:
objects
// Anonymous object typelet user: { id:number; name:string } = { id:1, name:"John",};// You can also specify optional properties with ?let product: { id:number; name:string; description?:string } = { id:101, name:"Laptop",// description is optional};
For more complex object types, it's better to define them using interfaces or type aliases, which we'll cover in a later section.
Sometimes you might have information about the type of a value that TypeScript doesn't know about. Type assertions are a way to tell the compiler "trust me, I know what I'm doing."
Trust me bro
// Using angle bracket syntaxlet someValue:any="this is a string";let strLength1:number= (<string>someValue).length;// Using as syntax (preferred, especially in JSX)let strLength2:number= (someValue asstring).length;
Type assertions don't change the runtime behavior of your code; they're purely a compile-time construct. They simply tell the TypeScript compiler to treat a value as a specific type.
// Example when working with DOM elementsconstinput=document.getElementById("inputField") asHTMLInputElement;// Now you can access input-specific propertiesconsole.log(input.value);
TypeScript can infer types in many situations, reducing the need for explicit type annotations.
Type Inference
// No need for type annotation, inferred as numberlet x =3;// Function return type is inferred as numberfunctionadd(a:number, b:number) {return a + b;}// inferred as (a: number, b: number) => numberletaddFunction=function (a:number, b:number) {return a + b;};
Type inference works well for simple types and can make your code cleaner. However, explicit type annotations can improve code readability and catch errors earlier.
Union types allow you to express a value that can be one of several types.
Union Types
// Can be either number or stringlet id:number|string;id =101; // OKid ="202"; // OK// id = true; // Error: Type 'boolean' is not assignable to type 'number | string'// Using union with arrayslet mixed: (number|string)[] = [1,"two",3,"four"];// Function that accepts multiple typesfunctionprintId(id:number|string) {console.log(`ID: ${id}`);// Type narrowingif (typeof id ==="string") {console.log(id.toUpperCase()); } else {console.log(id.toFixed(0)); }}
TypeScript also allows defining types as specific literal values, not just general types like string or number.
Literal Types
// String literal typetypeDirection="North"|"South"|"East"|"West";let direction:Direction="North"; // OK// let invalid: Direction = "Northwest"; // Error// Numeric literal typetypeDiceRoll=1|2|3|4|5|6;let roll:DiceRoll=3; // OK// let invalid: DiceRoll = 7; // Error// Boolean literal type (less common)typeBool=true;let isTrue:Bool=true; // OK// let isFalse: Bool = false; // Error
Literal types are particularly useful when combined with union types to define a specific set of allowed values.
// Function that only accepts specific string valuesfunctionsetAlignment(alignment:"left"|"center"|"right") {// ...}setAlignment("left"); // OK// setAlignment("top"); // Error
An intersection type combines multiple types into one. This allows you to add together existing types to get a single type that has all the features you need.
TypeScript uses control flow analysis to narrow types within conditional blocks. This is called type narrowing.
Type Guards
functionpadLeft(value:string, padding:string|number) {// Type narrowing with typeofif (typeof padding ==="number") {// In this branch, padding is known to be a numberreturn" ".repeat(padding) + value; }// In this branch, padding is known to be a stringreturn padding + value;}// Type guard using instanceoffunctionprocessValue(value:string|Date) {if (value instanceofDate) {// Here value is known to be Datereturnvalue.toISOString(); } else {// Here value is known to be stringreturnvalue.toUpperCase(); }}
You can also create custom type guards using type predicates:
interfaceBird {fly():void;layEggs():void;}interfaceFish {swim():void;layEggs():void;}// Type predicatefunctionisFish(pet:Fish|Bird): pet isFish {return (pet asFish).swim !==undefined;}functionmove(pet:Fish|Bird) {if (isFish(pet)) {// pet is now known to be Fishpet.swim(); } else {// pet is now known to be Birdpet.fly(); }}
TypeScript's type system provides a way to describe the shape of JavaScript objects that are passing through your code. Understanding these basic types is crucial for writing type-safe TypeScript code. Key points to remember:
TypeScript includes all JavaScript primitive types like boolean, number, and string
Special types like any, unknown, void, and never serve specific purposes
Complex types include arrays, tuples, enums, and objects
Union and intersection types allow composing types in flexible ways
Type guards help narrow types for safer operations
Type inference can reduce the need for explicit annotations
With these fundamentals, you can now build more complex type definitions and type-safe applications.