PowerShell Get-Type: Simplified Generics

PowerShell doesn’t include any syntactic sugar for dealing with generic objects, so a number of people have posted code snippets online showing how to create generic lists, dictionaries, etc. Interestingly, nobody has pointed out that the real problem isn’t creating the objects—New-Object does that without issue (for the most part)—but simply that it’s tedious to build correct closed type references for generics. This post attempts to address this core problem.

If we leverage the $args array, a trivial solution is quickly discovered:

function TypeOf-Generic ([type] $base) {
  $base.MakeGenericType($args)
}

Which is quite simple to use:

$intList = New-Object (TypeOf-Generic System.Collections.Generic.List int)

However, a bit more code yields a more complete solution:

function global:Get-Type (
    $type = $(throw "Please specify a type")
    )
{
  trap [System.Management.Automation.RuntimeException] { throw ($_.Exception.Message) }

  if($args -and $args.Count -gt 0)
  {
    $types = $args[0] -as [type[]]
    if(-not $types) { $types = [type[]] $args }

    if($genericType = [type] ($type + '`' + $types.Count))
    {
      $genericType.MakeGenericType($types)
    }
  }
  else
  {
    [type] $type
  }
}

Trapping RuntimeException allows us to repackage failed casts without the full ErrorRecord. We get a bit of usage flexibility by accepting our type arguments in either a single array or as the full $args array. With this function, all of the following will return valid types:

$a = Get-Type ([System.Collections.ArrayList])
$b = Get-Type System.Collections.Hashtable
$c = Get-Type System.Collections.Generic.List int
$d = Get-Type System.Collections.Generic.Dictionary int,string
$e = Get-Type System.Collections.Generic.Dictionary int $d

Corresponding to:

System.Collections.ArrayList
System.Collections.Hashtable
System.Collections.Generic.List`1[System.Int32]
System.Collections.Generic.Dictionary`2[System.Int32,System.String]
System.Collections.Generic.Dictionary`2[System.Int32,System.Collections.Generic.Dictionary`2[System.Int32,System.String]]

Having delegated the responsibility of resolving type references, we can update Lee Holmes‘s New-GenericObject:

function New-GenericObject(
    $type = $(throw "Please specify a type"),
    [object[]] $typeParameters = $null,
    [object[]] $constructorParameters = @()
    )
{
    $closedType = (Get-Type $type $typeParameters)
    ,[Activator]::CreateInstance($closedType, $constructorParameters)
}

I’ve included a few extra tweaks:

  1. I replaced [string] $typeName with [object] $type and changed $typeParameters from [string[]] to [object[]], specifically to accept [type] values if they are provided.
  2. If $typeParameters is $null, I assume we were passed a closed type; if not, Get-Type fails accordingly.
  3. $constructorParameters needs a default value to get around this exception:

    Exception calling “CreateInstance” with “2” argument(s): “Ambiguous match found.”

Cross-Assembly Generics

A user on Stack Overflow found an interesting quirk when trying to instantiate a generic SortedDictionary in PowerShell. The problem is easy to reproduce:

PS 1> $sdt = Get-Type System.Collections.Generic.SortedDictionary string,string
PS 2> $sdt.FullName
System.Collections.Generic.SortedDictionary`2[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToke
n=b77a5c561934e089],[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
PS 3> $sd = New-Object $sdt
New-Object : Cannot find type [System.Collections.Generic.SortedDictionary`2[System.String,System.String]]: make sure t
he assembly containing this type is loaded.
At line:1 char:17
+ $sd = New-Object  <<<< $sdt

Yet the type works fine with New-GenericObject:

PS 4> $sd = New-GenericObject $sdt
PS 5> $sd.GetType().FullName
System.Collections.Generic.SortedDictionary`2[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToke
n=b77a5c561934e089],[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]

As the answer on Stack Overflow points out, SortedDictionary and System.String live in different assemblies (System.dll and mscorlib.dll, respectively), which apparently confuses the PowerShell type converter. And because New-Object expects -typeName to be a string, it automatically casts our [type] object (which has the necessary assembly information) back to [string]:

PS 6> [string] $sdt
System.Collections.Generic.SortedDictionary`2[System.String,System.String]

If we pass in the type’s FullName instead, everything works as expected:

PS 7> $sd2 = New-Object $sdt.FullName
PS 8> $sd2.GetType().FullName
System.Collections.Generic.SortedDictionary`2[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToke
n=b77a5c561934e089],[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]

Note that this behavior extends to all assemblies, not just within the System namespace:

PS 11> $l = New-Object (Get-Type System.Collections.Generic.List ([Microsoft.SharePoint.SPWeb]))
New-Object : Cannot find type [System.Collections.Generic.List`1[Microsoft.SharePoint.SPWeb]]: make sure the assembly c
ontaining this type is loaded.
At line:1 char:16
+ $l = New-Object  <<<< (Get-Type System.Collections.Generic.List ([Microsoft.SharePoint.SPWeb]))
PS 12> $l = New-Object (Get-Type System.Collections.Generic.List ([Microsoft.SharePoint.SPWeb])).FullName
PS 13> $l.GetType().FullName
System.Collections.Generic.List`1[[Microsoft.SharePoint.SPWeb, Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral,
 PublicKeyToken=71e9bce111e9429c]]

If someone on the PowerShell team reads this, it would be awesome if New-Object had a parameter set that accepted a [type] instead of a [string]. Thanks in advance!

Advertisement