Cleaner Output with Custom Views in PowerShell: PSTypeName, Update-TypeData, and DefaultDisplayPropertySet
Readable objects beat walls of text. In PowerShell, you can make your commands return rich objects while still giving your team clean, predictable, and scannable output by leveraging custom types, type data, and default display property sets. In this guide, youll set default columns, add calculated properties once (for every instance), and keep formatting at the edges so data flows smoothly through the pipeline.
Return objects; format at the edges
As a producer of data (your functions, scripts, modules), your job is to return objects. As a consumer (interactive user, CI, reporting job), your job is to choose how to format or export them. Mixing these responsibilities leads to brittle pipelines.
Anti-pattern: formatting in the middle
# This destroys object fidelity. Export-Csv will receive formatting objects (useless for data).
Get-Project | Format-Table Name, Status | Export-Csv .\projects.csv
Do this instead
# Keep objects intact for downstream tools
Get-Project | Select-Object Name, Status | Export-Csv .\projects.csv -NoTypeInformation
# When you want pretty output at the console, format at the very end
Get-Project | Format-Table Name, Status, AgeDays -AutoSize
PowerShells formatting system kicks in only at the end of the pipeline. By giving your objects a type name and default display properties, you get tidy columns by default without forcing formatting early.
Define a custom type and default display with type data
You dont need a compiled class to define a type in PowerShell. Assign a PSTypeName to your PSCustomObject, then register type data for ScriptProperties and a DefaultDisplayPropertySet. From then on, every instance with that type name will pick up your custom view automatically.
# Define a custom type and view for cleaner output
$items = @(
[pscustomobject]@{ PSTypeName='Acme.Project'; Name='Alpha'; Status='Ready'; Created=(Get-Date).AddDays(-12) },
[pscustomobject]@{ PSTypeName='Acme.Project'; Name='Beta'; Status='Busy'; Created=(Get-Date).AddDays(-3) }
)
# Add a calculated ScriptProperty via type data (applies to all instances)
Update-TypeData -TypeName 'Acme.Project' -MemberType ScriptProperty -MemberName 'AgeDays' -Value { [int]((Get-Date) - $this.Created).TotalDays } -Force
# Set a concise default display
Update-TypeData -TypeName 'Acme.Project' -DefaultDisplayPropertySet 'Name','Status','AgeDays' -Force
# Now objects print with the chosen columns by default
$items
What you get: cleaner output, faster reviews, predictable displays, and happier pipelines. You can still access all properties ($items | Select-Object *) or export complete data structures (Export-Csv, ConvertTo-Json), but the default terminal view stays compact.
How DefaultDisplayPropertySet works
The DefaultDisplayPropertySet is a property set under the PSStandardMembers member set for your type. When PowerShell formats objects (e.g., Format-Table without explicit columns), it picks those properties. Keep the set small and meaningful (35 columns) for readability.
Add ScriptProperty once; get it everywhere
ScriptProperty attaches computed values to a type without modifying each object. Because the code runs against $this, it always evaluates on-demand with the latest data. You can also add NoteProperty for stored values or ScriptMethod for reusable calculations.
Make it stick across sessions
Update-TypeData is great for experimentation, but youll want durable configuration for CI runs, teammates, and future shells. You have three common options:
- Load a
.ps1xmlfile on demand usingUpdate-TypeData -PrependPath. - Ship type metadata with a module using
TypesToProcessin the module manifest. - For advanced table/list views (colors, alignment, custom tables), add a
Format.ps1xmlviaFormatsToProcessorUpdate-FormatData.
Option 1: Load a types file
# types.ps1xml (escape shown for documentation)
# Save this file and load it into the session.
# Load once per session (e.g., in $PROFILE or at module import)
Update-TypeData -PrependPath .\types.ps1xml -Force
Example types.ps1xml content:
<Types>
<Type>
<Name>Acme.Project</Name>
<Members>
<ScriptProperty>
<Name>AgeDays</Name>
<GetScriptBlock>[int]((Get-Date) - $this.Created).TotalDays</GetScriptBlock>
</ScriptProperty>
<MemberSet>
<Name>PSStandardMembers</Name>
<Members>
<PropertySet>
<Name>DefaultDisplayPropertySet</Name>
<ReferencedProperties>
<Name>Name</Name>
<Name>Status</Name>
<Name>AgeDays</Name>
</ReferencedProperties>
</PropertySet>
</Members>
</MemberSet>
</Members>
</Type>
</Types>
Option 2: Package with a module
Distribute your type data with your module so it loads automatically for consumers.
# Acme.Projects.psd1 (module manifest snippet)
@{
RootModule = 'Acme.Projects.psm1'
ModuleVersion = '1.0.0'
GUID = '00000000-0000-0000-0000-000000000001'
Author = 'You'
TypesToProcess = @('types.ps1xml') # Type metadata (ScriptProperty, DefaultDisplayPropertySet, etc.)
FormatsToProcess = @() # Add a .format.ps1xml later if you need richer formatting
RequiredModules = @()
}
When users import your module (Import-Module Acme.Projects), PowerShell registers your type data and your custom view is immediately active in their session.
Option 3: Full custom views with Format.ps1xml
If you need alignment, truncation behavior, custom labels, or list views beyond the default property set, define a .format.ps1xml. Use this for visualization, not for data definition. Keep the properties themselves in types.ps1xml so theyre available to all commands.
Practical patterns and gotchas
- Prefer narrow defaults. Pick the 35 fields that tell the story first (e.g., Name, Status, AgeDays). Let users drill down with
Select-Object *or format explicitly when they need more. - Keep computation in
ScriptProperty, not in your command output. Your commands should return data, not preformatted strings. This preserves sortability, filterability, and exportability. - Inspect your registrations with
(Get-TypeData 'Acme.Project').DefaultDisplayPropertySetandGet-TypeData 'Acme.Project' | Select-Object -Expand Members. - Reset during development with
Remove-TypeData -TypeName 'Acme.Project'or restart the session if youve loaded.ps1xmlfiles. - Understand
PSTypeNamesprecedence. PowerShell matches the first type name in$obj.PSTypeNameswhen resolving type/format data. Insert yours at index 0 for highest precedence:$obj.PSTypeNames.Insert(0, 'Acme.Project'). - Avoid mixing
Format-*with data cmdlets. Once you pipe toFormat-TableorFormat-List, the output is formatting objects, not your data. Dont pipe those intoExport-Csv,ConvertTo-Json, orWhere-Object. - Make your views predictable across sessions by shipping
types.ps1xmlin your module (or loading it from your profile). This eliminates red vs. green review churn where the same object renders differently on different machines. - Classes work too. If you define a PowerShell class
class Project {}with[Acme.Project]as its type, you can map the same type data by name. - Keep performance in mind.
ScriptPropertyruns on demand; keep getters fast, cache expensive computations in aNotePropertyif needed. - For table tuning, a
.format.ps1xmlcan add alignment and labels. But start withDefaultDisplayPropertySetits the simplest, most portable improvement.
End-to-end example
Heres a minimal pattern you can copy into your scripts or modules to make output cleaner immediately:
# 1) Produce objects with a PSTypeName so they pick up your type data
function Get-AcmeProject {
[CmdletBinding()]
param()
$raw = @(
@{ Name='Alpha'; Status='Ready'; Created=(Get-Date).AddDays(-12) },
@{ Name='Beta'; Status='Busy'; Created=(Get-Date).AddDays(-3) }
)
foreach ($r in $raw) {
[pscustomobject]@{ PSTypeName='Acme.Project'; Name=$r.Name; Status=$r.Status; Created=$r.Created }
}
}
# 2) Register type data (dev shell); in production, move this to types.ps1xml
Update-TypeData -TypeName 'Acme.Project' -MemberType ScriptProperty -MemberName AgeDays -Value { [int]((Get-Date) - $this.Created).TotalDays } -Force
Update-TypeData -TypeName 'Acme.Project' -DefaultDisplayPropertySet Name,Status,AgeDays -Force
# 3) Let consumers decide how to format or export
Get-AcmeProject # clean default columns
Get-AcmeProject | Sort AgeDays # object-aware operations
Get-AcmeProject | Export-Csv projects.csv -NoTypeInformation
Get-AcmeProject | Format-Table -AutoSize # pretty console view at the edge
With these pieces, your team sees what matters first, automation stays robust, and you can iterate on display without breaking consumers.
Make your results easy to scan. Explore the PowerShell Advanced Cookbook https://www.amazon.com/PowerShell-Advanced-Cookbook-scripting-advanced-ebook/dp/B0D5CPP2CQ/