TB

MoppleIT Tech Blog

Welcome to my personal blog where I share thoughts, ideas, and experiences.

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:

  1. Load a .ps1xml file on demand using Update-TypeData -PrependPath.
  2. Ship type metadata with a module using TypesToProcess in the module manifest.
  3. For advanced table/list views (colors, alignment, custom tables), add a Format.ps1xml via FormatsToProcess or Update-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').DefaultDisplayPropertySet and Get-TypeData 'Acme.Project' | Select-Object -Expand Members.
  • Reset during development with Remove-TypeData -TypeName 'Acme.Project' or restart the session if youve loaded .ps1xml files.
  • Understand PSTypeNames precedence. PowerShell matches the first type name in $obj.PSTypeNames when 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 to Format-Table or Format-List, the output is formatting objects, not your data. Dont pipe those into Export-Csv, ConvertTo-Json, or Where-Object.
  • Make your views predictable across sessions by shipping types.ps1xml in 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. ScriptProperty runs on demand; keep getters fast, cache expensive computations in a NoteProperty if needed.
  • For table tuning, a .format.ps1xml can add alignment and labels. But start with DefaultDisplayPropertySet  its 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/

← All Posts Home →