Enrich PowerShell Objects with Type Extensions: ScriptProperty and ScriptMethod in Practice
PowerShell lets you shape data, not just print it. By enriching .NET objects with type extensions, you move logic closer to the data, keep pipelines clean, and make scripts easier to reuse and maintain. Using Update-TypeData, you can attach ScriptProperty and ScriptMethod to existing types so your computed values are available anywhere in the session—on demand and always up to date via $this.
In this guide, you will learn when to use type extensions, how to implement ScriptProperty and ScriptMethod correctly, and how to package your extensions in startup scripts or .ps1xml files to keep sessions consistent across machines and CI/CD pipelines.
Why and When to Use Type Extensions
Type extensions help you add meaning right where you use it, without wrapping objects or sprinkling the same calculations across scripts.
- Cleaner pipelines: Replace repetitive
Select-Objectexpressions with first-class properties. - Reusable logic: Centralize calculations and helper methods so they work anywhere the type appears.
- On-demand freshness:
ScriptPropertyexecutes when accessed, so values reflect the current state. - Predictable output: Standardize defaults and property sets so teams see the same shape and columns.
Real-world use cases
- Ops file hygiene: Add
AgeDaysandSha256toSystem.IO.FileInfoto triage logs and verify releases. - Build artifacts: Add
SemVerparsing andIsPrerelease()methods to package objects. - Web/API automation: Enrich custom objects returned from REST calls with
IsHealthyorStaleness. - Cloud inventory: Enhance resource objects with
MonthlyCost,Owner, orTagMapfor clearer reporting.
How to Add ScriptProperty and ScriptMethod
Let’s start with a practical example: enrich System.IO.FileInfo with a SHA-256 hash and age in days. These properties compute on access, keeping results fresh without boilerplate in every pipeline.
ScriptProperty: compute on demand with $this
# Add computed properties to FileInfo
Update-TypeData -TypeName System.IO.FileInfo -MemberType ScriptProperty -MemberName Sha256 -Value {
(Get-FileHash -Path $this.FullName -Algorithm SHA256).Hash
} -Force
Update-TypeData -TypeName System.IO.FileInfo -MemberType ScriptProperty -MemberName AgeDays -Value {
[int](([datetime]::UtcNow - $this.LastWriteTimeUtc).TotalDays)
} -Force
# Use the new properties
$root = 'C:\\Logs'
Get-ChildItem -Path $root -File -Recurse |
Select-Object FullName, AgeDays, Sha256 |
Sort-Object AgeDays -Descending |
Select-Object -First 3Notes:
$thisrefers to the current instance. Here, it’s theFileInfowhose property you’re accessing.- Because the value is computed on access, it always reflects the file’s current state. That’s ideal for AgeDays. For expensive operations (like hashing large files), consider caching (shown below).
ScriptMethod: reusable behavior on the type
Encapsulate common checks or actions with ScriptMethod so you can call them directly on the object.
# Add a helper method to FileInfo
Update-TypeData -TypeName System.IO.FileInfo -MemberType ScriptMethod -MemberName IsOlderThan -Value {
param([int]$days)
$this.AgeDays -ge $days
} -Force
# Usage
Get-ChildItem -Path 'C:\\Logs' -File | Where-Object { $_.IsOlderThan(30) }Performance tip: cache expensive values
Script properties recompute each time you access them. For heavy operations (hashing, network calls), add a cached backing store. You can expose both a computed and cached variant, or implement a getter that lazily caches the value on first access.
# Lazy-cached SHA-256 value with a hidden NoteProperty
Update-TypeData -TypeName System.IO.FileInfo -MemberType ScriptProperty -MemberName Sha256Cached -Value {
$propName = 'Sha256CachedValue'
$pso = $this.PSObject
$prop = $pso.Properties[$propName]
if (-not $prop -or -not $prop.Value) {
$hash = (Get-FileHash -Path $this.FullName -Algorithm SHA256).Hash
if ($prop) { $prop.Value = $hash } else { $pso.Properties.Add([psnoteproperty]::new($propName, $hash)) }
}
$pso.Properties[$propName].Value
} -Force
# Optional: reset cache when you know the file changed
Update-TypeData -TypeName System.IO.FileInfo -MemberType ScriptMethod -MemberName ResetSha256Cache -Value {
$propName = 'Sha256CachedValue'
$p = $this.PSObject.Properties[$propName]
if ($p) { $p.Value = $null }
} -ForceMake default output helpful
You can change the default columns displayed for a type so your new properties show up automatically in tables.
# Set a default display property set for FileInfo
Update-TypeData -TypeName System.IO.FileInfo -DefaultDisplayPropertySet FullName, Length, AgeDays -Force
# Now gci shows AgeDays by default
Get-ChildItem -File | Select-Object -First 5Troubleshooting and hygiene
- Inspect what’s loaded:
Get-TypeData System.IO.FileInfo | Select-Object -Expand Members - Remove a member:
Remove-TypeData -TypeName System.IO.FileInfo(removes all extensions for that type from the current session) - Name carefully: choose unique member names to avoid collisions with future PowerShell versions or other modules.
Package, Test, and Ship Your Type Data
Ad-hoc updates are great for exploration, but production teams need repeatability. Persist your type data in one of these ways so every session—locally, in CI, or on servers—behaves the same.
Option 1: Profile script for your workstation
Drop your Update-TypeData calls into $PROFILE so the extensions load automatically.
# $PROFILE points to your current host profile file
if (-not (Test-Path $PROFILE)) { New-Item -ItemType File -Path $PROFILE -Force | Out-Null }
Add-Content -Path $PROFILE -Value @'
# FileInfo enrichments
Update-TypeData -TypeName System.IO.FileInfo -MemberType ScriptProperty -MemberName Sha256 -Value {
(Get-FileHash -Path $this.FullName -Algorithm SHA256).Hash
} -Force
Update-TypeData -TypeName System.IO.FileInfo -MemberType ScriptProperty -MemberName AgeDays -Value {
[int](([datetime]::UtcNow - $this.LastWriteTimeUtc).TotalDays)
} -Force
'@Pros: fast iteration. Cons: not easily shared across machines without separate provisioning.
Option 2: Types.ps1xml for portable, declarative definitions
.ps1xml files are the idiomatic way to define type data for distribution. Load them with Update-TypeData -PrependPath or include them in a module manifest.
<Types>
<Type>
<Name>System.IO.FileInfo</Name>
<Members>
<ScriptProperty>
<Name>Sha256</Name>
<GetScriptBlock>(Get-FileHash -Path $this.FullName -Algorithm SHA256).Hash</GetScriptBlock>
</ScriptProperty>
<ScriptProperty>
<Name>AgeDays</Name>
<GetScriptBlock>[int](([datetime]::UtcNow - $this.LastWriteTimeUtc).TotalDays)</GetScriptBlock>
</ScriptProperty>
<ScriptMethod>
<Name>IsOlderThan</Name>
<Script>param([int]$days) $this.AgeDays -ge $days</Script>
</ScriptMethod>
</Members>
<DefaultDisplayPropertySet>
<ReferencePropertyName>FullName</ReferencePropertyName>
<ReferencePropertyName>Length</ReferencePropertyName>
<ReferencePropertyName>AgeDays</ReferencePropertyName>
</DefaultDisplayPropertySet>
</Type>
</Types>Load it for the session:
Update-TypeData -PrependPath '.\\FileInfo.Types.ps1xml' -ForceOr ship it with a module by referencing it in MyModule.psd1:
@{
RootModule = 'MyModule.psm1'
ModuleVersion = '1.0.0'
GUID = '00000000-0000-0000-0000-000000000000'
Author = 'Your Team'
CompanyName = 'Contoso'
TypesToProcess = @('FileInfo.Types.ps1xml')
}Testing and CI/CD
- Pester assertions: Verify members exist and behave as expected.
Describe 'FileInfo type extensions' { It 'has AgeDays and Sha256' { $td = Get-TypeData System.IO.FileInfo $td.Members['AgeDays'] | Should -Not -BeNullOrEmpty $td.Members['Sha256'] | Should -Not -BeNullOrEmpty } It 'computes AgeDays' { $tmp = New-Item -ItemType File -Path (Join-Path $env:TEMP 't.txt') -Force ($tmp | Get-Item).AgeDays | Should -BeGreaterOrEqual 0 } } - Idempotence: Always use
-ForceforUpdate-TypeDataso reruns don’t fail. - Versioning: Keep type data changes in a module with semantic versioning; specify minimum module versions where used.
Security and safety considerations
- Trust the source: Script properties and methods execute code implicitly. Sign your modules and load only from trusted paths.
- Keep it deterministic: Prefer pure, side-effect-free getters. If you cache values, document reset methods (e.g.,
ResetSha256Cache()). - Avoid collisions: Use distinctive names; don’t shadow built-in members.
- Performance: Keep getters light; avoid network calls or large I/O in properties that might be enumerated over thousands of objects.
Putting it all together
With type extensions in place, your pipelines get dramatically cleaner:
# Find the three stalest log files and verify their hashes
Get-ChildItem -Path 'C:\\Logs' -File -Recurse |
Sort-Object AgeDays -Descending |
Select-Object -First 3 -Property FullName, AgeDays, Sha256The logic you once repeated in ad-hoc Select-Object blocks now travels with the data type—portable, testable, and discoverable via Get-Member. Whether you load it from a profile for your own shell, or ship it in a module for your team’s automation, type extensions are a low-friction, high-leverage way to level up your PowerShell workflows.