Skip to main content
PowerShell · lesson 3 of 12
PowerShell beginner Lesson 3 of 12

Variables, Arrays, Hashtables, and the $null Trap

PowerShell variables are dynamically typed and forgiving — until a hashtable property inside a string, or a null comparison in the wrong order, quietly does something other than what it looks like.

By SkillByExample · Published

powershell variables arrays hashtables null

Variables need no type declaration, and that flexibility is exactly where two of PowerShell’s best-known gotchas come from.

Variables have no declared type

$count = 5
$count.GetType().Name
$count = "5"
$count.GetType().Name
Int32
String

Same variable, no keyword, no re-declaration — assigning a new kind of value just changes what $count holds. This is normal and rarely causes problems on its own; the traps below come from comparing or interpolating values, not from assigning them.

Arrays

$fruits = "apple", "banana", "cherry"
$fruits[0]
$fruits[-1]
$fruits.Count
apple
cherry
3

Comma-separated values are already an array — -1 indexes from the end, same as Python. The range operator builds a sequence just as directly:

1..5
1
2
3
4
5

The bug: a property access inside a string

$book = @{ Title = "Dune"; Year = 1965 }
"Title: $book.Title"
Title: System.Collections.Hashtable.Title

Not even close to Dune. Inside a double-quoted string, PowerShell interpolates only the variable itself — $book — and everything after it, .Title, is left as literal text glued onto the end. $book on its own becomes its .ToString(), which for a hashtable is just its type name, and .Title gets appended to that string, not evaluated against the object.

The fix: force evaluation with $(…)

$book = @{ Title = "Dune"; Year = 1965 }
"Title: $($book.Title)"
Title: Dune

$(...) is a subexpression — anything inside it is evaluated first, and that result gets interpolated. Any property access, method call, or expression inside a string needs this wrapper; a bare $variable is the only thing simple interpolation understands.

Hashtables

$book = @{ Title = "Dune"; Year = 1965; Author = "Herbert" }
foreach ($key in $book.Keys) { "$key = $($book[$key])" }
Author = Herbert
Title = Dune
Year = 1965

A plain @{ } doesn’t promise to keep the order keys were added in — this one came back alphabetically here, but that’s incidental, not guaranteed. When the order actually matters, use [ordered]@{ } instead:

$book = [ordered]@{ Title = "Dune"; Year = 1965; Author = "Herbert" }
foreach ($key in $book.Keys) { "$key = $($book[$key])" }
Title = Dune
Year = 1965
Author = Herbert

The bug: comparing to $null in the wrong order

$x = @(1, $null, 4, $null)
if ($x -eq $null) { "x looks null (WRONG)" } else { "x is not null" }
x looks null (WRONG)

$x is a four-element array — it is very much not null. But $x -eq $null doesn’t test that; it filters the array element-by-element for values equal to $null, and returns a new two-element array of the matches. A two-or-more-element array is always truthy in an if, regardless of what it contains — so this reports “looks null” for exactly the wrong reason.

The fix: put $null on the left

$x = @(1, $null, 4, $null)
if ($null -eq $x) { "x looks null (WRONG)" } else { "x is not null" }
x is not null

$null -eq $x compares $null against $x as a single value on the right, so PowerShell never gets the chance to distribute the comparison across an array — it returns one real boolean, correctly, every time. This is why the PowerShell style convention is to always put $null on the left side of a comparison.

Practice

1. Slice an array to get a middle range of elements.
$scores = 85, 92, 78, 95, 88
$scores[1..3]
92
78
95

1..3 builds the index list 1, 2, 3 first, then indexes with all three at once — the same range operator from earlier in this lesson, just used as an index instead of a value.

2. Build a connection string from a hashtable's values, using $(...) correctly.
$config = @{ Host = "localhost"; Port = 8080 }
"Connecting to $($config.Host):$($config.Port)"
Connecting to localhost:8080

Two separate $(...) blocks in one string — each is evaluated independently before the whole string is assembled.

3. Confirm that $null -eq $x stays correct even when every element of $x is null.
$values = @($null, $null)
if ($null -eq $values) { "all null" } else { "not a scalar null" }
not a scalar null

$values is a two-element array, not $null itself, no matter what those two elements are — $null -eq $values correctly says so regardless of the array’s contents, which is exactly the property the wrong-order version doesn’t have.

Next: control flow and functions — including how PowerShell functions accept pipeline input.

Frequently Asked Questions

Do PowerShell variables need a type declaration?
No — $x = 5 makes $x an Int32, and $x = "5" makes the same variable a String, with no keyword needed either time. A type can be enforced explicitly ([int]$x = 5) when a script needs to guarantee it, but nothing requires it by default.
Why did "$book.Title" print the hashtable's type name followed by literal ".Title"?
String interpolation only expands the variable itself ($book) inside a plain "..." string — it doesn't parse anything after it as a property access, so .Title is left as literal text appended to $book's ToString() result. Wrapping the whole expression in $(...) tells PowerShell to evaluate it before interpolating, which is what actually reaches the property.
Why does $x -eq $null behave differently from $null -eq $x?
When $x is an array, $x -eq $null runs the comparison element-by-element and returns a new array of whatever matched — which is truthy in an if unless it happens to end up with exactly zero or one falsy element. $null -eq $x compares $null against $x as a single value and always returns one real boolean. Putting $null on the left is the safe, well-known convention for exactly this reason.
Is a PowerShell hashtable ordered?
A plain @{ } is a .NET Hashtable, which doesn't guarantee insertion order — small hashtables often happen to enumerate in a stable order, but it isn't guaranteed by the type and shouldn't be relied on. Write [ordered]@{ } instead when the order keys were added in actually matters, which converts it to an OrderedDictionary.
What's the difference between an array and @() in PowerShell?
They're the same thing — @(...) is just explicit array syntax, useful when a single value or an empty result needs to be forced into an array rather than PowerShell unwrapping it. $x = @(Get-Process) guarantees $x is an array even if only one process matched, where $x = Get-Process would hand back a bare object in that case.