TB

MoppleIT Tech Blog

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

Readable Objects with Update-TypeData in PowerShell: Teach Your Types to Speak

If you want cleaner consoles, clearer logs, and command output that explains itself, design your PowerShell objects to be readable by default. With Update-TypeData, you can set a DefaultDisplayPropertySet and add small ScriptMethods that make your objects "speak" without extra formatting. The data stays rich under the hood; the noise disappears from your terminals and pipelines.

In this post, you will learn how to use Update-TypeData to make objects self-explanatory, package these improvements into modules, and apply them safely in team and CI/CD environments.

Why teach your objects to speak

  • Less formatting boilerplate: stop piping to Format-Table for every command.
  • Clearer logs and reviews: defaults highlight intent and key fields.
  • Reusable types: once defined, every command returning the type benefits.
  • DevOps-friendly: consistent output in pipelines reduces flakiness in parsing and improves signal-to-noise in build logs.

The core pattern: Update-TypeData

Below is a minimal end-to-end example that introduces a custom PSTypeName, a default display set, and a small ScriptMethod for quick summaries.

$items = 1..3 | ForEach-Object {
  [pscustomobject]@{
    PSTypeName = 'Demo.Record'
    Name = ('item{0}' -f $_)
    Value = $_ * 10
    Created = Get-Date
  }
}

Update-TypeData -TypeName 'Demo.Record' -DefaultDisplayPropertySet 'Name','Value' -Force
Update-TypeData -TypeName 'Demo.Record' -MemberType ScriptMethod -MemberName 'Describe' -Value {
  '{0}={1}' -f $this.Name, $this.Value
} -Force

$items
$items[0].Describe()

What happens here:

  • PSTypeName marks the object as Demo.Record to the Extended Type System (ETS).
  • DefaultDisplayPropertySet reduces the default console view to Name and Value. The object still carries Created; it just stops cluttering the default view.
  • ScriptMethod Describe() gives you a quick, intentional summary for logs and debugging.

DefaultDisplayPropertySet: the quiet power move

Without changing your commands, the default output becomes concise. You can still request more when you need it:

$items         # shows Name,Value by default
$items | Format-List *   # shows everything
$items | Select-Object Name,Value,Created  # be explicit where needed

Add tiny ScriptMethods and ScriptProperties

Use ScriptMethods for short, deterministic helpers that explain the object. You can also add lightweight properties for frequent calculations.

Update-TypeData -TypeName 'Demo.Record' -MemberType ScriptProperty -MemberName 'AgeMinutes' -Value {
  [math]::Round(((Get-Date) - $this.Created).TotalMinutes,2)
} -Force

$items[0].AgeMinutes
$items[0].Describe()

Guidelines:

  • Keep them fast and side-effect free.
  • Prefer pure calculations based on existing properties.
  • Avoid network I/O, filesystem probes, or secrets access in anything that might run during display.

Package for reuse: modules, types.ps1xml, and CI

To make readable types available everywhere, put your type data in a module. You can apply Update-TypeData in the module's .psm1 on import, or ship a types.ps1xml file and reference it in the module manifest.

Option A: Apply type data in your module's .psm1

# MyModule.psm1
$tdParams = @{ TypeName = 'Demo.Record'; Force = $true }
Update-TypeData @tdParams -DefaultDisplayPropertySet 'Name','Value'
Update-TypeData @tdParams -MemberType ScriptMethod -MemberName 'Describe' -Value {
  '{0}={1}' -f $this.Name, $this.Value
}
Update-TypeData @tdParams -MemberType ScriptProperty -MemberName 'AgeMinutes' -Value {
  [math]::Round(((Get-Date) - $this.Created).TotalMinutes,2)
}

Option B: Use types.ps1xml (declarative, great for distribution)

A types.ps1xml file travels well, is easily reviewed, and avoids runtime code in type definitions.

<Types>
  <Type>
    <Name>Demo.Record</Name>
    <Members>
      <ScriptMethod>
        <Name>Describe</Name>
        <Script>'{0}={1}' -f $this.Name, $this.Value</Script>
      </ScriptMethod>
      <ScriptProperty>
        <Name>AgeMinutes</Name>
        <GetScriptBlock>[math]::Round(((Get-Date) - $this.Created).TotalMinutes,2)</GetScriptBlock>
      </ScriptProperty>
    </Members>
    <DefaultDisplayPropertySet>
      <ReferenceName>Name</ReferenceName>
      <ReferenceName>Value</ReferenceName>
    </DefaultDisplayPropertySet>
  </Type>
</Types>

Reference this file in your module manifest (.psd1):

@{
  RootModule       = 'MyModule.psm1'
  ModuleVersion    = '1.0.0'
  TypesToProcess   = @('types.ps1xml')
  # Optionally, add custom format views via FormatsToProcess
}

DevOps pipelines and logs: keep it readable and structured

Readable objects shine in CI/CD, scheduled jobs, and containerized automation:

  • Short defaults keep console logs compact and tail-friendly.
  • Objects remain fully structured for durable logging (e.g., JSON).
  • The same objects are readable in both local and remote sessions.

Example: concise console, rich structured log.

$items | ForEach-Object {
  # Console-friendly single line
  Write-Host $_.Describe()

  # Machine-friendly structured record (script methods are not serialized)
  $_ | Select-Object Name,Value,Created | ConvertTo-Json -Depth 3
}

Notes:

  • DefaultDisplayPropertySet affects console formatting, not serialization.
  • ScriptMethods and ScriptProperties are not included by ConvertTo-Json unless you explicitly Select-Object them.
  • For durable logs, select explicit properties so log schema stays stable.

Practical tips, performance, and safety

Do

  • Keep default display sets small (2–5 properties).
  • Favor value-like 'Demo.Record' Remove-TypeData -TypeName 'Demo.Record' -ErrorAction SilentlyContinue
    • When multiple definitions exist, later updates win unless constrained by scope. Use -Force to overwrite in the current session.

    Advanced: teach ToString() carefully

    You can add a custom ToString for single-column displays or quick stringification. Use sparingly and keep it stable.

    Update-TypeData -TypeName 'Demo.Record' -MemberType ScriptMethod -MemberName 'ToString' -Value {
      '{0} (Value={1}, Age={2}m)' -f $this.Name, $this.Value, [math]::Round(((Get-Date) - $this.Created).TotalMinutes,0)
    } -Force
    
    # Now, string contexts show a concise summary
    'this is ' + $items[0]

    Considerations:

    • Do not put secrets or environment data in ToString. Strings may end up in logs unexpectedly.
    • Keep it fast and deterministic.

    Real-world use cases

    • Infrastructure automation: resources returned by provisioning scripts (clusters, queues, web apps) show Name and Status by default, while retaining IDs and metadata for export.
    • Release pipelines: deployment result objects display Target, Version, and Outcome in a readable row; logs capture full details in JSON artifacts.
    • Observability: health check results show Check, State, and Duration; teams use Describe() for compact console banners, while structured sinks receive the full object.

    Bringing it all together

    1. Give your objects a PSTypeName.
    2. Define a DefaultDisplayPropertySet to reduce noise.
    3. Add tiny ScriptMethods/ScriptProperties for intent-revealing helpers.
    4. Package type data in a module (types.ps1xml or Update-TypeData in .psm1).
    5. Use explicit property selection for machine logs; let defaults serve humans.
    6. Test in CI and watch for performance and safety pitfalls.

    Sharpen your object design in PowerShell and make every command tell a clear story. Explore the PowerShell Advanced CookBook here: PowerShell Advanced CookBook.

    ← All Posts Home →