TypeScript Basic Types
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.
CH Skip navigation typescript primitive types Create 6 Avatar image TypeScript Fundamentals - Primitives and Base Types
©Harry Wolff
Primitive Types
TypeScript includes the same primitive types available in JavaScript, but with added type safety.
Boolean
The most basic data type is the simple true/false value, known as a boolean.
let isDone: boolean = false;
let isActive: boolean = true;
// Type inference works too - TypeScript knows this is a boolean
let isComplete = false;
Number
As in JavaScript, all numbers in TypeScript are floating point values. These can be decimal, hexadecimal, binary, or octal literals.
let decimal: number = 10;
let hex: number = 0xf00d; // hexadecimal
let binary: number = 0b1010; // binary
let octal: number = 0o744; // octal
// Operations
let sum: number = 10 + 5; // 15
let difference: number = 10 - 5; // 5
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.
let firstName: string = "John";
let lastName: string = "Doe";
// Template literals can span multiple lines and embed expressions
let 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. Every symbol is completely unique, even if created with the same description. This prevents accidental property overwrites when multiple libraries or pieces of code work with the same object.
let sym1: symbol = Symbol();
let sym2: symbol = Symbol("key"); // optional string key
// Each symbol is unique
let areEqual: boolean = sym1 === sym2; // always false
BigInt
Regular JavaScript numbers become imprecise beyond 2^53. BigInt
maintains exact precision for any size integer, crucial for financial calculations, cryptography, and scientific computing.
// Only available when targeting ES2020 or later
let bigNumber: bigint = 9007199254740991n;
let anotherBigNumber: bigint = BigInt(9007199254740991);
Special Types
null and undefined
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:
// Variables explicitly set to null
let userContact: string | null = null; // We know there's no contact info
let selectedItem: object | null = null; // No item is selected
// Variables that are undefined
let userName: string; // Declared but not initialized, so it's undefined
console.log(userName); // undefined
function findUser(id: number): object | undefined {
// Return undefined when no user is found
if (id < 0) return undefined;
// ...
}
// The difference in checking
let 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
:
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:
with --strictNullChecks
// With strictNullChecks
let num: number = null; // Error
let id: number = undefined; // Error
let maybeNum: number | null = null; // OK with union type
let 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:
let notSure: any = 4;
notSure = "maybe a string";
notSure = false; // okay, definitely a boolean
// Using any disables type checking
notSure.toFixed(); // No compile error, might fail at runtime
notSure.someNonExistentMethod(); // No error either!
Important: The any
type is generally frowned upon in TypeScript and should be avoided at all times.
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.
let valueAny: any = 10;
let valueUnknown: unknown = 10;
// With any, you can do anything - no type safety!
valueAny.toFixed(2); // No error
valueAny.someNonExistentMethod(); // No error
valueAny.foo.bar.baz; // No error
let 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 checkingunknown
: Enforces type checking before operations
Assignment compatibility:
any
: Can be assigned to any other typeunknown
: Can only be assigned to any or unknown types
Operations allowed:
any
: All operations allowed without checksunknown
: No operations allowed until type is narrowed
Property access:
any
: Any property can be accessedunknown
: 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 type
if (typeof value === "string") {
console.log(value.toUpperCase()); // OK: now treated as string
}
// Alternative using type assertion (use with caution)
console.log((value as string).toUpperCase());
// Using instanceof for objects
let someDate: unknown = new Date();
if (someDate instanceof Date) {
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.
function logMessage(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:
// Arrow function that returns void
const logError = (error: Error): void => {
console.error(error.message);
};
// Callback function type with void return
type ClickHandler = (event: MouseEvent) => void;
// Using void in function type definitions
interface ButtonProps {
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 props
interface SubmitButtonProps {
// void return type means the parent doesn't expect a return value
onSubmit: () => void;
onCancel: () => void;
}
const SubmitButton = ({ onSubmit, onCancel }: SubmitButtonProps) => {
return (
<div>
<button onClick={onSubmit}>Submit</button>
<button onClick={onCancel}>Cancel</button>
</div>
);
};
// Using the component
const Form = () => {
// These handlers don't need to return anything
const handleSubmit = () => {
console.log('Submitted');
// Implementation details...
};
const handleCancel = () => {
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:
function warnUser(): void {
console.log("Warning!");
return true; // Valid, but the return value is ignored
}
const result = 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:
type VoidCallback = () => void;
const callback: VoidCallback = () => {
return "hello"; // Allowed despite void return type
};
// The return is allowed, but you're not supposed to use it
const value = 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.
// Function that never returns
function error(message: string): never {
throw new Error(message);
}
// Function that always throws
function fail(): never {
return error("Something failed");
}
// Function with an infinite loop
function infiniteLoop(): 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 typeunknown
: Top type - can be assigned from any type, but not to other types without narrowingnever
: 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:
type Shape = Circle | Square | Triangle;
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
interface Triangle {
kind: "triangle";
base: number;
height: number;
}
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.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 here
const exhaustiveCheck: 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 valueunknown
: A function returning unknown could return any value, but the caller must verify the typenever
: 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.
Complex Types
Complex Types
©Computeshorts
Arrays
TArrays represent ordered collections of elements of the same type or multiple types. They provide type safety for list operations and can be homogeneous (all same type) or heterogeneous (mixed types). Arrays support various forms including readonly arrays that prevent mutation and nested arrays for multi-dimensional data structures.
// Using the type followed by []
let list1: number[] = [1, 2, 3];
// Using the generic Array type
let list2: Array<number> = [1, 2, 3];
// Array of mixed types with a union type
let mixed: (number | string)[] = [1, "two", 3];
// Type inference works with arrays too
let 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.
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 number
let moreNumbers = numbers.map((n) => n * 2); // Still number[]
Tuples
Tuples represent arrays with a fixed number of elements where each position has a specific type. Unlike regular arrays where all elements share the same type, tuples define exactly what type each position must contain and enforce a specific length.
They're perfect for representing structured data like coordinates, database records, function return values with multiple pieces of information, or any scenario where you need a fixed-length collection with known types at each position. Tuples can have optional elements, rest elements, and readonly modifiers to prevent mutation.
// Declare a tuple type
let person: [string, number] = ["John", 25];
// Access with correct types
let 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]
type CSVRow = [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:
// Named tuple elements
type HttpResponse = [code: number, body: string, headers?: object];
const response: HttpResponse = [200, '{"success": true}', { "Content-Type": "application/json" }];
Enums
Enums create named constants that represent a fixed set of values. They're useful for representing states, categories, or any finite set of options like user roles, HTTP status codes, or configuration flags. Enums can be numeric, string-based, or computed, and they provide both type safety and runtime values.
enum Direction {
Up,
Down,
Left,
Right,
}
let move: Direction = Direction.Up;
By default, enums begin numbering their members starting at 0, but you can override this by explicitly setting values:
enum StatusCode {
OK = 200,
Created = 201,
BadRequest = 400,
Unauthorized = 401,
NotFound = 404,
}
function handleResponse(code: StatusCode) {
switch (code) {
case StatusCode.OK:
console.log("Everything is fine");
break;
case StatusCode.NotFound:
console.log("Resource not found");
break;
// ...
}
}
String enums are also supported, which are more readable:
string enums
enum PrintMedia {
Newspaper = "NEWSPAPER",
Newsletter = "NEWSLETTER",
Magazine = "MAGAZINE",
Book = "BOOK",
}
let media: PrintMedia = PrintMedia.Magazine;
console.log(media); // "MAGAZINE"
How Enums Improve Code Maintainability
Enums significantly enhance code maintainability by providing meaningful, self-documenting constants and improving type safety. Consider this comparison:
Without enums (using string literals):
// Using string literals directly
function processResponse(status: string) {
if (status === "success") {
// Process successful response
} else if (status === "error") {
// Handle error
} else if (status === "pending") {
// Handle pending state
}
// What if someone passes "SUCCESS" or "Success" instead?
}
// Calling the function
processResponse("success");
processResponse("SUCCESS"); // No error, but might not work as expected
processResponse("pending");
processResponse("wating"); // Typo, but TypeScript won't catch this!
With enums:
enum ResponseStatus {
Success = "success",
Error = "error",
Pending = "pending",
}
function processResponse(status: ResponseStatus) {
switch (status) {
case ResponseStatus.Success:
// Process successful response
break;
case ResponseStatus.Error:
// Handle error
break;
case ResponseStatus.Pending:
// Handle pending state
break;
}
}
// Calling the function
processResponse(ResponseStatus.Success); // Correct
processResponse(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.
Real-World Example: API Request Status
enum RequestStatus {
Idle = "idle",
Loading = "loading",
Success = "success",
Error = "error"
}
interface RequestState<T> {
status: RequestStatus;
data: T | null;
error: Error | null;
}
function fetchUserData(userId: string): RequestState<User> {
// Initial state
let state: RequestState<User> = {
status: RequestStatus.Idle,
data: null,
error: null
};
try {
state.status = RequestStatus.Loading;
// Fetch data...
state.status = RequestStatus.Success;
state.data = /* fetched user */;
} catch (error) {
state.status = RequestStatus.Error;
state.error = error instanceof Error ? error : new Error(String(error));
}
return state;
}
// Using the state
const userState = fetchUserData("123");
if (userState.status === RequestStatus.Error) {
showErrorMessage(userState.error!.message);
} else if (userState.status === RequestStatus.Success) {
displayUserProfile(userState.data!);
}
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
Objects represent structured data with named properties, each having their own types. They form the backbone of most TypeScript applications, defining the shape of data structures, API responses, configuration objects, and component props. Objects can have required properties, optional properties, and readonly properties.
// Anonymous object type
let 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.
Type Assertions
Type assertions tell TypeScript to treat a value as a specific type, overriding the compiler's type analysis. They're used when you know more about a value's type than TypeScript can determine automatically.
Common scenarios include working with DOM elements where you know the specific element type, handling data from external APIs where you understand the structure better than the compiler, or dealing with complex type transformations.
Type assertions don't change the runtime value - they're purely compile-time instructions that can be dangerous if used incorrectly since they bypass type checkingType assertions are a way to tell the compiler "trust me, I know what I'm doing."
Trust me bro
// Using angle bracket syntax
let someValue: any = "this is a string";
let strLength1: number = (<string>someValue).length;
// Using as syntax (preferred, especially in JSX)
let strLength2: number = (someValue as string).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 elements
const input = document.getElementById("inputField") as HTMLInputElement;
// Now you can access input-specific properties
console.log(input.value);
Type Inference
Type inference is TypeScript's ability to automatically determine types without explicit annotations. The compiler analyzes variable assignments, function return values, and expressions to deduce the most appropriate types. This reduces the need for verbose type annotations while maintaining type safety. TypeScript infers types from initial values, function parameters and returns, array elements, object properties, and control flow analysis.
The inference system is sophisticated enough to understand complex scenarios like conditional types, generic constraints, and union type narrowing, making code cleaner while preserving strong typing.
// No need for type annotation, inferred as number
let x = 3;
// Function return type is inferred as number
function add(a: number, b: number) {
return a + b;
}
// inferred as (a: number, b: number) => number
let addFunction = 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
Union types represent values that can be one of several types. They're useful when a variable legitimately needs to accept different types of data. For example, a function parameter might accept either a string or a number, or a variable might hold either user data or an error message.
// Can be either number or string
let id: number | string;
id = 101; // OK
id = "202"; // OK
// id = true; // Error: Type 'boolean' is not assignable to type 'number | string'
// Using union with arrays
let mixed: (number | string)[] = [1, "two", 3, "four"];
// Function that accepts multiple types
function printId(id: number | string) {
console.log(`ID: ${id}`);
// Type narrowing
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id.toFixed(0));
}
}
Literal Types
Literal types represent specific values rather than general types. String, number, and boolean literals create types that only accept those exact values. They're useful for creating precise constraints and enumeration-like behavior.
// String literal type
type Direction = "North" | "South" | "East" | "West";
let direction: Direction = "North"; // OK
// let invalid: Direction = "Northwest"; // Error
// Numeric literal type
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
let roll: DiceRoll = 3; // OK
// let invalid: DiceRoll = 7; // Error
// Boolean literal type (less common)
type Bool = 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 values
function setAlignment(alignment: "left" | "center" | "right") {
// ...
}
setAlignment("left"); // OK
// setAlignment("top"); // Error
Type Aliases
Type aliases create custom names for any type, making complex type definitions more readable and reusable. They use the type
keyword to define new names for existing types, whether simple primitives, complex object shapes, union types, or intricate generic constructions.
Type aliases are particularly valuable for creating domain-specific vocabulary in your codebase, simplifying repetitive type expressions, and establishing consistent type definitions across modules. Unlike interfaces, type aliases can represent any type including primitives, unions, intersections, tuples, and computed types. They support generic parameters, conditional logic, and can be recursive for self-referencing data structures.
Type aliases are compile-time only constructs that get erased during JavaScript compilation, but they provide essential documentation and type safety during development. They're especially useful for creating branded types, utility type combinations, and complex API response shapes that would otherwise be unwieldy to write repeatedly throughout an application.
type UserID = number | string;
type Point = { x: number; y: number };
// Using type aliases
let userId: UserID = 123;
let coordinates: Point = { x: 10, y: 20 };
// Type aliases can be more complex
type Result<T> = { success: true; value: T } | { success: false; error: string };
function getResult(): Result<number> {
// ...
if (Math.random() > 0.5) {
return { success: true, value: 42 };
} else {
return { success: false, error: "Something went wrong" };
}
}
Intersection Types
Intersection types combine multiple types into one, creating a new type that has all properties from each constituent type. This is commonly used for mixing object types together, like combining a base user type with additional permission properties.
type Employee = {
id: number;
name: string;
};
type Manager = {
department: string;
level: number;
};
// Combine Employee and Manager types
type ManagerWithEmployeeInfo = Employee & Manager;
let manager: ManagerWithEmployeeInfo = {
id: 123,
name: "John Smith",
department: "IT",
level: 2,
};
Type Guards and Predicates
Type guards and predicates are special functions that help TypeScript understand type narrowing
at runtime. They provide a way to safely check and cast types, enabling more sophisticated type checking in conditional logic.
Each of these complex types serves specific use cases in creating robust, type-safe applications while maintaining flexibility and expressiveness in your type definitions.
function padLeft(value: string, padding: string | number) {
// Type narrowing with typeof
if (typeof padding === "number") {
// In this branch, padding is known to be a number
return " ".repeat(padding) + value;
}
// In this branch, padding is known to be a string
return padding + value;
}
// Type guard using instanceof
function processValue(value: string | Date) {
if (value instanceof Date) {
// Here value is known to be Date
return value.toISOString();
} else {
// Here value is known to be string
return value.toUpperCase();
}
}
You can also create custom type guards using type predicates:
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
// Type predicate
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
// pet is now known to be Fish
pet.swim();
} else {
// pet is now known to be Bird
pet.fly();
}
}
Exercises
Exercise 1: Type Annotations
Create variables with proper type annotations for the following values:
A user's name
A user's age
Whether the user is active or not
A list of the user's hobbies
The user's mixed-type information tuple: [id, name, active]
A function that takes a user's name and returns a greeting message
A variable that could be either a number or null
Exercise 2: Working with Object Types
Define a type for a Product with the following properties:
id (number)
name (string)
price (number)
category (string)
inStock (boolean)
tags (array of strings, optional)
Then create an array of products and write a function that filters products by category.
Exercise 3: Type Guards and Union Types
Create a function that processes different kinds of data:
If given a
number
, double it and return itIf given a
string
, return its lengthIf given a
boolean
, return its negationIf given an
array
of numbers, return the sum of all elementsIf given anything else, throw an error with message "Unsupported data type"
Summary
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
, andstring
Special types like
any
,unknown
,void
, and never serve specific purposesComplex types include
arrays
,tuples
,enums
, andobjects
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.