Structures#

PowerShell variables are typed .NET objects under the hood, even when the operator never names a type. The everyday types are scalars (string, int, double, bool, datetime), arrays (@()), hashtables (@{}), the dynamic ``PSCustomObject`` for structured records, and full classes when the work earns one. The underlying .NET types make PowerShell objects interoperate cleanly with every cmdlet in the ecosystem.

Types#

A variable picks up its type from the value bound to it. Cast with [Type] to lock it in.

Type

Declaration

When to reach for it

String

$x = 'value'

Text. Backed by System.String.

Int

[int]$n = 42

32-bit integer. [long] for 64-bit.

Double / Decimal

[double]$r = 3.14

Floating-point. [decimal] for money.

Bool

$ok = $true

$true / $false.

DateTime

[datetime]'2026-05-15'

Time and date.

Array

@('a','b','c')

Ordered list, 0-based, fixed size after creation.

List

[System.Collections.Generic.List[int]]::new()

Resizable, typed list.

Hashtable

@{Key='v'; Other=1}

Key / value map. Iteration order preserved.

OrderedDictionary

[ordered]@{...}

Hashtable with guaranteed insertion order.

PSCustomObject

[PSCustomObject]@{Name='a'; Age=1}

The “structured record”; what the pipeline carries.

Class

class T { [int]$x }

First-class types with methods and inheritance.

String#

Backed by System.String. Methods come from .NET; immutable.

$name = 'operator'
$name.ToUpper()                        # OPERATOR
$name.Substring(0, 3)                  # ope
$name.Length                           # 8

Int / Double / Decimal#

PowerShell parses numeric literals as the narrowest type that fits. Cast for control.

$n = 42                                # int
$r = 3.14                              # double
[decimal]$money = 19.95                # decimal (money math)
$big = 1KB + 2MB                       # numeric type suffixes

Bool#

$true and $false are the canonical booleans. Truthiness: $null, 0, '', empty arrays, and $false are all falsy.

if ($items.Count) { 'have items' }     # truthy if non-zero

Array#

Ordered list, 0-based, fixed length after creation. Append with += returns a new array; use [List[T]] for hot loops.

$hosts = @('web01', 'web02', 'db01')
$hosts += 'cache01'                    # creates a new array
$hosts[0]                              # web01
$hosts[-1]                             # cache01 (negative indexing)
$hosts[1..2]                           # slice: web02, db01
$hosts.Count                           # 4

For high-volume appends, use a generic list:

$buf = [System.Collections.Generic.List[string]]::new()
foreach ($h in $hosts) { $buf.Add($h) }
$buf.ToArray()

The single-element-array trap: @(...) forces array semantics even on a one-item result. Without it, Get-ChildItem of a folder with one file returns the bare FileInfo, not an array.

$files = @(Get-ChildItem *.log)        # always an array

Hashtable#

Key / value map with O(1) lookup. Literal form @{...}.

$region = @{
    web01   = 'us-east-1'
    db01    = 'eu-west-1'
    cache01 = 'us-west-2'
}
$region['web01']                       # us-east-1
$region.web01                          # same; dot notation
$region.Keys
$region.ContainsKey('web01')

foreach ($k in $region.Keys) {
    "$k -> $($region[$k])"
}

Use [ordered]@{...} when iteration order matters. Standard hashtables preserve insertion order in modern PowerShell, but [ordered] makes the intent explicit and works in older versions.

PSCustomObject#

The “structured record” that pipelines carry. Looks like a hashtable literal with [PSCustomObject] cast in front. Property access is dotted and case-insensitive. The standard return structure for advanced functions that need to emit multi-field rows.

$user = [PSCustomObject]@{
    Name = 'operator'
    Role = 'analyst'
    Port = 22
}
$user.Name

# Build a table:
$rows = $hosts | ForEach-Object {
    [PSCustomObject]@{
        Host = $_
        Region = $region[$_]
    }
}
$rows | Format-Table

Class#

PowerShell 5+ has real classes with constructors, methods, properties, static members, and inheritance.

class Host {
    [string]$Name
    [string]$Region
    [int]   $Port

    Host([string]$n, [string]$r) {
        $this.Name = $n
        $this.Region = $r
        $this.Port = 22
    }

    [string] ToString() {
        return "$($this.Name)@$($this.Region)"
    }
}

$h = [Host]::new('web01', 'us-east-1')
$h.ToString()

Reach for a class when behavior travels with the data; a PSCustomObject plus a function usually suffices.

Scope#

Variables live in a scope: Global, Script, Private, Local (the default). Subordinate scopes can read parent values but not write them; $script:counter or $global:logger reaches across.

$script:counter = 0
function Tick { $script:counter++ }
Tick; Tick; Tick
$script:counter                        # 3

Picking the right type#

Type

Reach for it when…

Watch out for

String

Single value, text.

It is immutable; build long strings with -join or StringBuilder.

Array @()

Fixed-size ordered list, small.

+= allocates a new array each time.

List[T]

Hot-loop appends, typed elements.

.NET-flavored; less ergonomic than @().

Hashtable @{}

String → anything map.

Default iteration order is insertion order in modern PowerShell; use [ordered] to be safe.

PSCustomObject

Structured records flowing through the pipeline.

Property access is by name; no schema enforced.

Class

Behavior + data; the same type used many places.

More ceremony than PSCustomObject; only worth it when methods help.

Hosts#

The everyday array structure.

$hosts = @('web01', 'web02', 'db01', 'cache01')
foreach ($h in $hosts) {
    ssh -o ConnectTimeout=3 $h 'uptime'
}

Drive the list from a file or a command.

$hosts = Get-Content hosts.txt
$hosts = (Get-Content inventory.txt) -match '^prod-' -replace '\s.*'

Host → metadata#

Hashtable for the lookup; the operator types the host name they already know.

$region = @{
    web01   = 'us-east-1'
    db01    = 'eu-west-1'
    cache01 = 'us-west-2'
}
Connect-AzAccount -Region $region[$h]

Record per host#

PSCustomObject per row; Format-Table to render. The same structure feeds Export-Csv, ConvertTo-Json, and any cmdlet that wants a table.

$rows = $hosts | ForEach-Object {
    [PSCustomObject]@{
        Host    = $_
        Region  = $region[$_]
        Port    = 22
    }
}
$rows | Format-Table
$rows | Export-Csv hosts.csv -NoTypeInformation
$rows | ConvertTo-Json

Sets via maps#

There is no set type. Use a hashtable with a sentinel value, or System.Collections.Generic.HashSet[T] for the .NET version.

$seen = @{}
foreach ($ip in (Get-Content access.log | ForEach-Object { ($_ -split ' ')[0] })) {
    if (-not $seen.ContainsKey($ip)) {
        $seen[$ip] = $true
        $ip                            # first sighting
    }
}

$set = [System.Collections.Generic.HashSet[string]]::new()
$null = $set.Add('a'); $null = $set.Add('a')
$set.Count                             # 1

References#

  • Get-Help about_Variables, Get-Help about_Hash_Tables, Get-Help about_Arrays, Get-Help about_PSCustomObject, Get-Help about_Classes.

  • Overview for the language-wide patterns these structures plug into.

  • I/O and Pipelines for how these structures flow through the pipeline.

  • Algorithms for Sort-Object / Group-Object / Compare-Object over these structures.

  • Microsoft Learn / Working with objects