PowerShell 5.0 introduced a game-changing feature that transformed the way administrators and developers write scripts: native class support. This addition brought true object-oriented programming (OOP) capabilities to PowerShell, enabling you to create reusable, maintainable, and professionally structured code.

This comprehensive guide explores PowerShell classes and custom types, providing practical examples and best practices for leveraging these powerful features in your automation workflows.

Understanding PowerShell Classes

PowerShell classes allow you to define custom object blueprints with properties, methods, constructors, and inheritance. Unlike PSCustomObject, classes provide compile-time type checking, inheritance support, and a more structured approach to code organization.

Leveraging PowerShell Classes and Custom Types: Complete Guide to Object-Oriented Programming in PowerShell v5+

Basic Class Syntax

Creating a basic class in PowerShell follows a straightforward syntax pattern:

class Server {
    # Properties
    [string]$Name
    [string]$IPAddress
    [int]$Port
    [bool]$IsOnline

    # Constructor
    Server([string]$name, [string]$ip) {
        $this.Name = $name
        $this.IPAddress = $ip
        $this.Port = 443
        $this.IsOnline = $false
    }

    # Method
    [void]Connect() {
        Write-Host "Connecting to $($this.Name) at $($this.IPAddress):$($this.Port)"
        $this.IsOnline = $true
    }

    # Method with return value
    [string]GetStatus() {
        return "$($this.Name) is $(if($this.IsOnline){'Online'}else{'Offline'})"
    }
}

# Create instance
$webServer = [Server]::new("WebServer01", "192.168.1.100")
$webServer.Connect()
Write-Host $webServer.GetStatus()

Output:

Connecting to WebServer01 at 192.168.1.100:443
WebServer01 is Online

Properties and Property Validation

PowerShell classes support various property types and validation attributes to ensure data integrity.

Property Types and Attributes

class User {
    # Strongly typed properties
    [string]$Username
    
    # Validation attributes
    [ValidateRange(18, 120)]
    [int]$Age
    
    [ValidateSet("Admin", "User", "Guest")]
    [string]$Role
    
    [ValidatePattern("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")]
    [string]$Email
    
    # Hidden property (not displayed by default)
    hidden [string]$PasswordHash
    
    # Static property (shared across all instances)
    static [int]$TotalUsers = 0

    # Constructor
    User([string]$username, [int]$age, [string]$email) {
        $this.Username = $username
        $this.Age = $age
        $this.Email = $email
        $this.Role = "User"
        [User]::TotalUsers++
    }
}

# Create users
$user1 = [User]::new("john_doe", 30, "[email protected]")
$user2 = [User]::new("jane_smith", 25, "[email protected]")

Write-Host "Total Users Created: $([User]::TotalUsers)"
Write-Host "User1 Role: $($user1.Role)"

Output:

Total Users Created: 2
User1 Role: User

Constructors and Overloading

PowerShell classes support multiple constructors with different parameter signatures, enabling flexible object instantiation.

class DatabaseConnection {
    [string]$Server
    [string]$Database
    [int]$Port
    [string]$ConnectionString

    # Default constructor
    DatabaseConnection() {
        $this.Server = "localhost"
        $this.Database = "master"
        $this.Port = 1433
        $this.BuildConnectionString()
    }

    # Constructor with server and database
    DatabaseConnection([string]$server, [string]$database) {
        $this.Server = $server
        $this.Database = $database
        $this.Port = 1433
        $this.BuildConnectionString()
    }

    # Constructor with all parameters
    DatabaseConnection([string]$server, [string]$database, [int]$port) {
        $this.Server = $server
        $this.Database = $database
        $this.Port = $port
        $this.BuildConnectionString()
    }

    hidden [void]BuildConnectionString() {
        $this.ConnectionString = "Server=$($this.Server),$($this.Port);Database=$($this.Database)"
    }

    [string]ToString() {
        return $this.ConnectionString
    }
}

# Different ways to instantiate
$conn1 = [DatabaseConnection]::new()
$conn2 = [DatabaseConnection]::new("sql-server", "ProductionDB")
$conn3 = [DatabaseConnection]::new("sql-server", "ProductionDB", 1435)

Write-Host "Connection 1: $conn1"
Write-Host "Connection 2: $conn2"
Write-Host "Connection 3: $conn3"

Output:

Connection 1: Server=localhost,1433;Database=master
Connection 2: Server=sql-server,1433;Database=ProductionDB
Connection 3: Server=sql-server,1435;Database=ProductionDB

Methods and Method Types

PowerShell classes support instance methods, static methods, and method overloading for versatile functionality.

Leveraging PowerShell Classes and Custom Types: Complete Guide to Object-Oriented Programming in PowerShell v5+

class Calculator {
    [double]$LastResult
    static [string]$Version = "1.0"

    # Instance method
    [double]Add([double]$a, [double]$b) {
        $this.LastResult = $a + $b
        return $this.LastResult
    }

    # Method overloading - same name, different parameters
    [double]Add([double[]]$numbers) {
        $this.LastResult = ($numbers | Measure-Object -Sum).Sum
        return $this.LastResult
    }

    # Static method (no access to instance properties)
    static [double]Multiply([double]$a, [double]$b) {
        return $a * $b
    }

    # Method with complex logic
    [hashtable]Calculate([string]$operation, [double]$a, [double]$b) {
        $result = switch ($operation) {
            "add" { $a + $b }
            "subtract" { $a - $b }
            "multiply" { $a * $b }
            "divide" { 
                if ($b -eq 0) { throw "Division by zero" }
                $a / $b 
            }
            default { throw "Unknown operation: $operation" }
        }
        
        $this.LastResult = $result
        
        return @{
            Operation = $operation
            Result = $result
            Timestamp = Get-Date
        }
    }
}

$calc = [Calculator]::new()

# Instance methods
Write-Host "5 + 3 = $($calc.Add(5, 3))"
Write-Host "Sum of array: $($calc.Add(@(10, 20, 30, 40)))"

# Static method (no instance needed)
Write-Host "Static multiply: $([Calculator]::Multiply(7, 6))"

# Complex method
$result = $calc.Calculate("divide", 100, 4)
Write-Host "Division result: $($result.Result) at $($result.Timestamp)"

Output:

5 + 3 = 8
Sum of array: 100
Static multiply: 42
Division result: 25 at 10/22/2025 16:52:00

Inheritance and Polymorphism

PowerShell classes support single inheritance, allowing you to build hierarchical class structures and implement polymorphic behavior.

Leveraging PowerShell Classes and Custom Types: Complete Guide to Object-Oriented Programming in PowerShell v5+

class Animal {
    [string]$Name
    [int]$Age
    [string]$Species

    Animal([string]$name, [int]$age) {
        $this.Name = $name
        $this.Age = $age
    }

    # Virtual method (can be overridden)
    [string]MakeSound() {
        return "Some generic animal sound"
    }

    [string]GetInfo() {
        return "$($this.Name) is a $($this.Age) year old $($this.Species)"
    }
}

class Dog : Animal {
    [string]$Breed

    Dog([string]$name, [int]$age, [string]$breed) : base($name, $age) {
        $this.Species = "Dog"
        $this.Breed = $breed
    }

    # Override parent method
    [string]MakeSound() {
        return "Woof! Woof!"
    }

    [void]Fetch() {
        Write-Host "$($this.Name) is fetching the ball!"
    }
}

class Cat : Animal {
    [bool]$IsIndoor

    Cat([string]$name, [int]$age, [bool]$isIndoor) : base($name, $age) {
        $this.Species = "Cat"
        $this.IsIndoor = $isIndoor
    }

    # Override parent method
    [string]MakeSound() {
        return "Meow!"
    }

    [void]Scratch() {
        $location = if ($this.IsIndoor) { "furniture" } else { "tree" }
        Write-Host "$($this.Name) is scratching the $location"
    }
}

# Polymorphism in action
$animals = @(
    [Dog]::new("Rex", 5, "German Shepherd"),
    [Cat]::new("Whiskers", 3, $true),
    [Dog]::new("Buddy", 2, "Golden Retriever")
)

foreach ($animal in $animals) {
    Write-Host $animal.GetInfo()
    Write-Host "Sound: $($animal.MakeSound())"
    Write-Host "---"
}

# Type-specific methods
$dog = $animals[0]
$dog.Fetch()

Output:

Rex is a 5 year old Dog
Sound: Woof! Woof!
---
Whiskers is a 3 year old Cat
Sound: Meow!
---
Buddy is a 2 year old Golden Retriever
Sound: Woof! Woof!
---
Rex is fetching the ball!

Implementing Interfaces with PowerShell Classes

While PowerShell doesn’t have native interface support like C#, you can implement interface-like patterns using base classes and method contracts.

class ILogger {
    [void]LogInfo([string]$message) {
        throw "Method must be implemented by derived class"
    }
    
    [void]LogError([string]$message) {
        throw "Method must be implemented by derived class"
    }
}

class FileLogger : ILogger {
    [string]$LogPath

    FileLogger([string]$logPath) {
        $this.LogPath = $logPath
    }

    [void]LogInfo([string]$message) {
        $entry = "[INFO] $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - $message"
        Add-Content -Path $this.LogPath -Value $entry
        Write-Host $entry -ForegroundColor Green
    }

    [void]LogError([string]$message) {
        $entry = "[ERROR] $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - $message"
        Add-Content -Path $this.LogPath -Value $entry
        Write-Host $entry -ForegroundColor Red
    }
}

class ConsoleLogger : ILogger {
    [void]LogInfo([string]$message) {
        Write-Host "[INFO] $message" -ForegroundColor Cyan
    }

    [void]LogError([string]$message) {
        Write-Host "[ERROR] $message" -ForegroundColor Red
    }
}

# Usage with polymorphism
function Process-WithLogging([ILogger]$logger) {
    $logger.LogInfo("Process started")
    
    try {
        # Simulate work
        Start-Sleep -Milliseconds 100
        $logger.LogInfo("Task completed successfully")
    }
    catch {
        $logger.LogError("Task failed: $_")
    }
}

$fileLogger = [FileLogger]::new("C:\temp\app.log")
$consoleLogger = [ConsoleLogger]::new()

Process-WithLogging -logger $fileLogger
Process-WithLogging -logger $consoleLogger

Real-World Example: Building a Configuration Manager

This comprehensive example demonstrates how to build a practical configuration management system using PowerShell classes.

Leveraging PowerShell Classes and Custom Types: Complete Guide to Object-Oriented Programming in PowerShell v5+

class ConfigItem {
    [string]$Key
    [object]$Value
    [string]$Type
    [bool]$IsRequired
    [string]$Description

    ConfigItem([string]$key, [object]$value, [string]$type, [bool]$isRequired) {
        $this.Key = $key
        $this.Value = $value
        $this.Type = $type
        $this.IsRequired = $isRequired
    }

    [string]ToString() {
        return "$($this.Key) = $($this.Value) [$($this.Type)]"
    }
}

class ConfigValidator {
    static [bool]ValidateType([ConfigItem]$item) {
        switch ($item.Type) {
            "string" { return $item.Value -is [string] }
            "int" { return $item.Value -is [int] }
            "bool" { return $item.Value -is [bool] }
            "array" { return $item.Value -is [array] }
            default { return $true }
        }
    }

    static [string[]]ValidateAll([ConfigItem[]]$items) {
        $errors = @()
        foreach ($item in $items) {
            if ($item.IsRequired -and [string]::IsNullOrEmpty($item.Value)) {
                $errors += "Required configuration '$($item.Key)' is missing"
            }
            if (-not [ConfigValidator]::ValidateType($item)) {
                $errors += "Configuration '$($item.Key)' has invalid type. Expected: $($item.Type)"
            }
        }
        return $errors
    }
}

class ConfigManager {
    hidden [hashtable]$Configurations
    [string]$ConfigPath

    ConfigManager([string]$configPath) {
        $this.ConfigPath = $configPath
        $this.Configurations = @{}
        $this.InitializeDefaults()
    }

    hidden [void]InitializeDefaults() {
        $this.AddConfig([ConfigItem]::new("ServerName", "localhost", "string", $true))
        $this.AddConfig([ConfigItem]::new("Port", 8080, "int", $true))
        $this.AddConfig([ConfigItem]::new("EnableLogging", $true, "bool", $false))
        $this.AddConfig([ConfigItem]::new("MaxConnections", 100, "int", $false))
    }

    [void]AddConfig([ConfigItem]$item) {
        $this.Configurations[$item.Key] = $item
    }

    [object]GetValue([string]$key) {
        if ($this.Configurations.ContainsKey($key)) {
            return $this.Configurations[$key].Value
        }
        throw "Configuration key '$key' not found"
    }

    [void]SetValue([string]$key, [object]$value) {
        if ($this.Configurations.ContainsKey($key)) {
            $this.Configurations[$key].Value = $value
        } else {
            throw "Configuration key '$key' not found"
        }
    }

    [bool]Validate() {
        $items = $this.Configurations.Values
        $errors = [ConfigValidator]::ValidateAll($items)
        
        if ($errors.Count -gt 0) {
            Write-Host "Configuration validation failed:" -ForegroundColor Red
            $errors | ForEach-Object { Write-Host "  - $_" -ForegroundColor Red }
            return $false
        }
        
        Write-Host "Configuration validation passed" -ForegroundColor Green
        return $true
    }

    [void]DisplayConfig() {
        Write-Host "`nCurrent Configuration:" -ForegroundColor Cyan
        Write-Host ("=" * 50)
        foreach ($key in $this.Configurations.Keys | Sort-Object) {
            $item = $this.Configurations[$key]
            $required = if ($item.IsRequired) { "[Required]" } else { "[Optional]" }
            Write-Host "$($item.Key): $($item.Value) $required"
        }
        Write-Host ("=" * 50)
    }

    [hashtable]ExportConfig() {
        $export = @{}
        foreach ($key in $this.Configurations.Keys) {
            $export[$key] = $this.Configurations[$key].Value
        }
        return $export
    }
}

# Example usage
$configMgr = [ConfigManager]::new("C:\config\app.json")

Write-Host "Initial Configuration:"
$configMgr.DisplayConfig()

# Modify configuration
Write-Host "`nModifying configuration..."
$configMgr.SetValue("Port", 9090)
$configMgr.SetValue("MaxConnections", 200)

Write-Host "`nUpdated Configuration:"
$configMgr.DisplayConfig()

# Validate
Write-Host ""
$isValid = $configMgr.Validate()

# Export
if ($isValid) {
    $exported = $configMgr.ExportConfig()
    Write-Host "`nExported Configuration:"
    $exported.GetEnumerator() | Sort-Object Name | ForEach-Object {
        Write-Host "  $($_.Key): $($_.Value)"
    }
}

Advanced Patterns: Factory and Singleton

PowerShell classes support advanced design patterns for building scalable applications.

Factory Pattern

class DatabaseConnection {
    [string]$ConnectionString
    [string]$Type

    DatabaseConnection([string]$connectionString, [string]$type) {
        $this.ConnectionString = $connectionString
        $this.Type = $type
    }

    [void]Connect() {
        Write-Host "Connecting to $($this.Type) database..."
    }
}

class SqlServerConnection : DatabaseConnection {
    SqlServerConnection([string]$server, [string]$database) : base(
        "Server=$server;Database=$database;Integrated Security=true",
        "SQL Server"
    ) {}

    [void]Connect() {
        Write-Host "Opening SQL Server connection: $($this.ConnectionString)"
    }
}

class MySqlConnection : DatabaseConnection {
    MySqlConnection([string]$server, [string]$database) : base(
        "Server=$server;Database=$database;Uid=root;Pwd=password",
        "MySQL"
    ) {}

    [void]Connect() {
        Write-Host "Opening MySQL connection: $($this.ConnectionString)"
    }
}

class DatabaseFactory {
    static [DatabaseConnection]CreateConnection([string]$type, [string]$server, [string]$database) {
        switch ($type.ToLower()) {
            "sqlserver" { return [SqlServerConnection]::new($server, $database) }
            "mysql" { return [MySqlConnection]::new($server, $database) }
            default { throw "Unknown database type: $type" }
        }
    }
}

# Usage
$sqlConn = [DatabaseFactory]::CreateConnection("sqlserver", "localhost", "TestDB")
$sqlConn.Connect()

$mysqlConn = [DatabaseFactory]::CreateConnection("mysql", "192.168.1.50", "AppDB")
$mysqlConn.Connect()

Singleton Pattern

class Logger {
    static hidden [Logger]$Instance
    hidden [System.Collections.ArrayList]$Logs

    # Private constructor
    hidden Logger() {
        $this.Logs = [System.Collections.ArrayList]::new()
    }

    static [Logger]GetInstance() {
        if ($null -eq [Logger]::Instance) {
            [Logger]::Instance = [Logger]::new()
        }
        return [Logger]::Instance
    }

    [void]Log([string]$message) {
        $entry = "[$(Get-Date -Format 'HH:mm:ss')] $message"
        $this.Logs.Add($entry) | Out-Null
        Write-Host $entry
    }

    [string[]]GetLogs() {
        return $this.Logs.ToArray()
    }

    [int]GetLogCount() {
        return $this.Logs.Count
    }
}

# Usage - same instance everywhere
$logger1 = [Logger]::GetInstance()
$logger1.Log("First message")

$logger2 = [Logger]::GetInstance()
$logger2.Log("Second message")

# Both references point to same instance
Write-Host "`nTotal logs: $($logger2.GetLogCount())"
Write-Host "All logs:"
$logger2.GetLogs() | ForEach-Object { Write-Host "  $_" }

Best Practices and Performance Considerations

Type Safety and Validation

Always use strongly typed properties and validation attributes to prevent runtime errors:

class BestPracticeExample {
    # Good: Strongly typed with validation
    [ValidateNotNullOrEmpty()]
    [string]$Name
    
    [ValidateRange(1, 100)]
    [int]$Priority
    
    # Good: Use enums for limited options
    [ValidateSet("Low", "Medium", "High")]
    [string]$Severity
    
    # Bad: Avoid untyped properties
    # $GenericProperty
}

Memory Management

class ResourceManager {
    [System.IO.FileStream]$FileHandle
    
    ResourceManager([string]$path) {
        $this.FileHandle = [System.IO.File]::OpenRead($path)
    }
    
    # Implement cleanup
    [void]Dispose() {
        if ($null -ne $this.FileHandle) {
            $this.FileHandle.Dispose()
            $this.FileHandle = $null
        }
    }
}

# Usage with proper cleanup
$resource = [ResourceManager]::new("C:\data.txt")
try {
    # Use resource
}
finally {
    $resource.Dispose()
}

Performance Tips

  • Use static methods for utility functions that don’t need instance state
  • Mark internal helper methods as hidden to reduce memory overhead
  • Prefer strongly typed collections over generic arrays
  • Implement ToString() for better debugging experience
  • Cache expensive computations in properties rather than recalculating

Debugging and Testing Classes

class TestableCalculator {
    [double]$LastResult
    hidden [System.Collections.ArrayList]$History
    
    TestableCalculator() {
        $this.History = [System.Collections.ArrayList]::new()
    }
    
    [double]Add([double]$a, [double]$b) {
        $result = $a + $b
        $this.RecordOperation("Add", $a, $b, $result)
        return $result
    }
    
    hidden [void]RecordOperation([string]$op, [double]$a, [double]$b, [double]$result) {
        $this.History.Add(@{
            Operation = $op
            Operand1 = $a
            Operand2 = $b
            Result = $result
            Timestamp = Get-Date
        }) | Out-Null
        $this.LastResult = $result
    }
    
    [object[]]GetHistory() {
        return $this.History.ToArray()
    }
    
    [void]ClearHistory() {
        $this.History.Clear()
    }
}

# Testing
$calc = [TestableCalculator]::new()
$calc.Add(10, 5)
$calc.Add(20, 15)

Write-Host "Operation History:"
$calc.GetHistory() | ForEach-Object {
    Write-Host "$($_.Operation): $($_.Operand1) + $($_.Operand2) = $($_.Result)"
}

Common Pitfalls and Solutions

Avoiding Circular References

# Problem: Circular reference
class Parent {
    [Child]$Child
}

class Child {
    [Parent]$Parent  # This creates circular reference
}

# Solution: Use weak references or redesign
class Parent {
    [string]$ChildId  # Store reference ID instead
}

class Child {
    [string]$Id
    [string]$ParentId
}

Proper Error Handling

class RobustService {
    [void]ProcessData([string]$data) {
        try {
            if ([string]::IsNullOrEmpty($data)) {
                throw [System.ArgumentException]::new("Data cannot be null or empty")
            }
            
            # Process data
            Write-Host "Processing: $data"
        }
        catch [System.ArgumentException] {
            Write-Error "Invalid argument: $($_.Exception.Message)"
            throw
        }
        catch {
            Write-Error "Unexpected error: $($_.Exception.Message)"
            throw
        }
    }
}

Conclusion

PowerShell classes and custom types provide powerful object-oriented programming capabilities that transform script development. By leveraging classes, you can create reusable, maintainable code that follows industry best practices. Whether you’re building configuration managers, implementing design patterns, or creating domain-specific abstractions, PowerShell classes offer the structure and type safety needed for professional automation solutions.

Start incorporating classes into your PowerShell scripts today to improve code organization, enable better testing, and build more scalable automation frameworks. The examples and patterns demonstrated in this guide provide a solid foundation for mastering object-oriented PowerShell development.