The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Create an ordered record$Item = [pscustomobject][ordered]@{ Name = 'api'; State ` = 'Running' }View examples
Add a semantic type name$Item.PSObject.TypeNames.Insert(0, ` 'Contoso.ServiceStatus')View examples
Declare a classclass ServiceSpec { [string] $Name; [bool] $Enabled }View examples
Invoke a constructor$Spec = [ServiceSpec]::new('api', $true)View examples
Declare an enumenum HealthState { Unknown; Healthy; Degraded; Failed }View examples
Validate a property value[ValidateSet('Dev','Test','Prod')] [string] $EnvironmentView examples
Hide an implementation memberhidden [string] $CacheKeyView examples
Call a static method[Widget]::NormalizeName(' API ')View examples
Add a calculated property$Item | Add-Member -NotePropertyName Healthy -NotePropertyValue $trueView examples
Project calculated output$Item | Select-Object Name,@{n='Label';e={"$($_.Name):$($_.State)"}}View examples
Accept typed pipeline inputparam([Parameter(ValueFromPipeline)][ServiceSpec] ` $InputObject)View examples
Document output type[OutputType([ServiceSpec])] param()View examples
Serialize with explicit depth$Item | ConvertTo-Json -Depth 5View examples
Persist PowerShell data$Item | Export-Clixml -LiteralPath '.\item.clixml'View examples
Load module types at parse timeusing module Contoso.ModelsView examples
Set default display propertiesUpdate-TypeData -TypeName Contoso.ServiceStatus ` -DefaultDisplayPropertySet Name,State -ForceView examples

PowerShell objects range from lightweight pipeline records to formal user-defined types. PSCustomObject is usually best for shaped output and data transfer; classes add constructors, methods, inheritance, and stronger property contracts when behavior and identity matter. Understand parse-time type loading, pipeline binding, method side effects, and serialization before using classes across modules, jobs, remoting, or persistent formats.

Step by step

Detailed examples

01

Choose records for data and classes for behavior

Use PSCustomObject for pipeline records, reports, API payloads, and short-lived structured data. Use a class when callers benefit from constructors, methods, inheritance, or an explicit runtime type. PowerShell classes require version 5.0 or newer; these in-memory examples need no Windows role, domain, elevation, remoting, reboot, or WhatIf. Neither choice creates a security boundary. Ordered hashtables make initial property display deterministic, while a custom PSTypeName lets format and type data target records without requiring a class. Prefer small output objects over returning live administrative objects whose methods can mutate systems.

Return a typed status record
$Status = [pscustomobject][ordered]@{
    Name = 'api'
    State = 'Running'
    CheckedAt = [datetimeoffset]::UtcNow
}
$Status.PSObject.TypeNames.Insert(0, 'Contoso.ServiceStatus')
$Status
Back to quick reference ↑
02

Define constructors with explicit invariants

PowerShell classes arrived in version 5.0. Properties default to the type's default value, and constructors do not automatically chain arbitrary overloads. Validate required values and establish all invariants in every public constructor. Static New methods can provide readable factories. Class definitions affect the current session and generally cannot be redefined without starting a new session, which matters during iterative module development.

Create a small validated model
class ServiceSpec {
    [string] $Name
    [bool] $Enabled
    ServiceSpec([string] $name, [bool] $enabled) {
        if ([string]::IsNullOrWhiteSpace($name)) { throw 'Name is required' }
        $this.Name = $name.Trim()
        $this.Enabled = $enabled
    }
}
$Spec = [ServiceSpec]::new('api', $true)
Back to quick reference ↑
03

Use type conversion and validation knowingly

Typed properties convert compatible assigned values and reject incompatible ones, but conversion may be surprising: strings, numbers, collections, and enums follow PowerShell conversion rules. ValidateSet and other validation attributes can strengthen assignments. Enums improve readability but serialize as names or numbers depending on the serializer and target contract. Avoid using a type annotation as proof that untrusted input is safe; validate semantics, ranges, paths, and authorization separately.

Model a bounded state
enum HealthState { Unknown; Healthy; Degraded; Failed }
class ProbeResult {
    [ValidateNotNullOrEmpty()] [string] $Name
    [HealthState] $State = [HealthState]::Unknown
    [ValidateRange(0, 60000)] [int] $LatencyMs
}
$Result = [ProbeResult]::new()
$Result.Name = 'api'
$Result.State = 'Healthy'
Back to quick reference ↑
04

Keep methods predictable and hidden members non-secret

Instance methods receive $this; static members belong to the type. Explicit return types convert returned values and require all paths to satisfy the contract. Hidden only suppresses normal Get-Member, completion, and display; reflection and direct access can still reveal the member, so never store secrets there expecting confidentiality. Keep methods free of unexpected network or administrative side effects, and name mutating behavior clearly.

Add deterministic instance and static behavior
class Widget {
    [string] $Name
    hidden [string] $CacheKey
    static [string] NormalizeName([string] $value) { return $value.Trim().ToLowerInvariant() }
    [string] ToString() { return $this.Name }
}
$Normalized = [Widget]::NormalizeName(' API ')
Back to quick reference ↑
05

Project output instead of mutating shared input

Add-Member extends an individual PSObject wrapper and is useful when enrichment is intentional. Select-Object calculated properties create a new projected record, which is usually safer inside reusable functions because the caller's input remains untouched. Script properties and script methods execute code when accessed and can surprise consumers or serializers; reserve them for controlled objects. Use property names that remain stable for automation, then configure presentation separately.

Create a non-mutating projection
$Service = Get-Service -Name 'EventLog'
$View = $Service | Select-Object Name, Status, @{
    Name = 'IsRunning'
    Expression = { $_.Status -eq [ServiceProcess.ServiceControllerStatus]::Running }
}
$View
Back to quick reference ↑
06

Design class-aware pipeline contracts

ValueFromPipeline binds whole objects; ValueFromPipelineByPropertyName binds compatible properties. A class-typed parameter rejects deserialized or shape-compatible objects that are not actual instances, so public remoting commands often work better with primitive parameters or validated PSCustomObject input. OutputType is documentation and tooling metadata only—it does not restrict emitted objects. Emit objects one at a time and avoid Format-Table inside functions because formatting records are not reusable data.

Process typed instances without formatting them
function Test-ServiceSpec {
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param([Parameter(Mandatory, ValueFromPipeline)][ServiceSpec] $InputObject)
    process {
        [pscustomobject]@{ Name = $InputObject.Name; Valid = $InputObject.Name.Length -le 256 }
    }
}
Back to quick reference ↑
07

Assume serialization preserves data, not behavior

JSON has no PowerShell class identity, methods, SecureString semantics, or rich type fidelity. ConvertTo-Json truncates beyond its Depth and may represent enums or dates differently than an API expects. CLIXML preserves more PowerShell metadata, but remoting and jobs commonly return deserialized objects whose type names begin with Deserialized and whose methods are gone. Never deserialize untrusted formats that can instantiate arbitrary types; JSON is preferable for cross-system data contracts, with explicit schema validation after import.

Round-trip a data transfer object
$Payload = [pscustomobject]@{ Name = 'api'; Enabled = $true; Version = 2 }
$Json = $Payload | ConvertTo-Json -Depth 3 -Compress
$Copy = $Json | ConvertFrom-Json
$Copy | Select-Object Name, Enabled, Version
Back to quick reference ↑
08

Package classes with parse-time loading in mind

A class in a script module must be available while callers are parsed. using module is a parse-time statement and is the supported way to consume exported PowerShell classes; Import-Module alone may not make type literals usable during parsing. Changing a loaded class generally requires a fresh PowerShell process. Avoid exposing internal implementation classes unnecessarily because public types become compatibility commitments. Use type and format data for display rather than overriding ToString for every presentation need.

Inspect and apply non-destructive type data
using module Contoso.Models
Update-TypeData -TypeName 'Contoso.ServiceStatus' `
    -DefaultDisplayPropertySet Name, State, CheckedAt -Force
Get-TypeData -TypeName 'Contoso.ServiceStatus'
Back to quick reference ↑

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. Microsoft Learnabout Classeslearn.microsoft.com
  2. Microsoft Learnabout Class Constructorslearn.microsoft.com
  3. Microsoft Learnabout Class Methodslearn.microsoft.com
  4. Microsoft LearnEverything you wanted to know about PSCustomObjectlearn.microsoft.com
  5. Microsoft LearnAdd-Memberlearn.microsoft.com
  6. Microsoft Learnabout Type Acceleratorslearn.microsoft.com

Help us improve

Found a typo or missing example?

Tell us what would make this cheat sheet clearer, more complete, or more useful.

Share feedback