PowerShell Modules Exporting classes
Wouldn’t it be great if you could export classes from a PowerShell module? You know, when you import a module, its public functions are available in your session. Can’t we have the same way for classes?
Feel free to follow this post with the code from the example repository here: ModuleExportClasses. This content is from the advanced part of the PowerShell Fundamentals course, and we use it in Mastering PowerShell Universal.
But, we can’t!?
If you followed the PowerShell evolution, you probably know that there’s no way to export a class from a module. Nothing the module manifests, nor in the Export-ModuleMember function either…

The using Statement
A PowerShell script must start with the using module statement to use types defined in a module. Specify either the module’s name or its path to identify where the types are defined.
With the module defining the following class with its static method:
# .\MyModule\MyModule.psm1
class MyModuleClass
{
static [string] DoStuff()
{
return 'I''m busy!'
}
}To use that static method in a script or another module, we then have to write the using statement as below:
# ./myScript.ps1
using module .\MyModule
# do stuff
[MyModuleClass]::DoStuff()That’s a bit cumbersome, and sometimes not completely intuitive.
Try doing the using statements in the shell for the module .\MyModule and .\OtherModule…

But if you run the using statements in the same block, it works:

Ok, that does the job. However, it would be much better if we could just export the classes we cherry pick from our PowerShell Module. This way, our users can use them directly when importing the module!
Introducing Type Accelerators
Type Accelerators are Aliases for .NET types, generally used to have a shortened name for a long type, without using the using namespace statement…
You can find the registered type accelerators using the static method from the TypeAccelerators class:
# Get the internal TypeAccelerators class to use its static methods.
$typeAcceleratorsClass = [psobject].Assembly.GetType(
'System.Management.Automation.TypeAccelerators'
)
$typeAcceleratorsClass::Get
# ...
$typeAcceleratorsClass::Get.GetEnumerator() | Select -First 3
Key Value
--- -----
Alias System.Management.Automation.AliasAttribute
AllowEmptyCollection System.Management.Automation.AllowEmptyCollectionAttribute
AllowEmptyString System.Management.Automation.AllowEmptyStringAttributeIf you select the first 3, you ought to see that [Alias] is actually a type accelerator for [System.Management.Automation.AliasAttribute].
So what if we registered Type Accelerators for our Module classes during import?
Exporting Type Accelerators for Module classes
We now add the following snippet to the bottom of the PSM1. We have to append it after the classes have been declared.
# At the bottom of ./TheModule/TheModule.psm1
# inspired from https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_classes?view=powershell-7.5
$typesToExport = @(
'TheModuleClass'
)
# Get the internal TypeAccelerators class to use its static methods.
$typeAcceleratorsClass = [psobject].Assembly.GetType(
'System.Management.Automation.TypeAccelerators'
)
$existingTypeAccelerators = $typeAcceleratorsClass::Get
foreach ($typeToExport in $typesToExport)
{
# validate the type exists
$type = $TypeToExport -as [System.Type]
if (-not $type)
{
Write-Warning -Message (
'Unable to export {0}. Type not found.' -f $typeToExport
)
}
else
{
$null = $TypeAcceleratorsClass::Add($TypeToExport, $Type)
}
}And we test in the shell:

Without using the using module .\TheModule statement, we can now use the class from the module once the module is imported. We can even cherry pick which classes will be available when the module is imported.
Now let’s double check in the TypeAccelerators class what’s available:
$typeAcceleratorsClass = [psobject].Assembly.GetType(
'System.Management.Automation.TypeAccelerators'
)
$existingTypeAccelerators = $typeAcceleratorsClass::Get
$existingTypeAccelerators.GetEnumerator() | Select -last 1And we get the latest added Type Accelerator:

Using exported types in other modules
Now if I start from a clean shell, and I import the module named UsingTheModule that has a function like so:
function Invoke-TheModuleClassMethod
{
[CmdletBinding()]
param
(
# No param required
)
[TheModuleClass]::DoStuff()
}Importing the module will work just fine. This is because the use of [TheModuleClass] is within the script definition (scriptblock). However, invoking the command will fail with InvalidOperation: Unable to find type [TheModuleClass]..

The types are exported (the corresponding Type Accelerator registered) only when the module is imported. This is because that’s when the code in TheModule.psm1 is executed.
Importing the module and running the same code again:

To use the type (accelerator) with our module without a manual extra step, import the module first. Otherwise, the PowerShell parser will complain that the type does not exist.
In a module, this is achieved with the RequiredModules setting in the module manifest. In the UsingTheModule.psd1 module manifest, we can add TheModule to the list of RequiredModules, now it should just work, right?
Starting from a fresh shell again:

Another error!
But it knows it must load the required module TheModule, it just can’t find it! That’s a $Env:PSModulePath issue, let’s add the current directory.
$psmodpath = $Env:PSModulePath -split [io.path]::PathSeparator
if ($psmodpath -notcontains $PWD.Path) { $psmodpath = @($PWD.Path) + $psmodpath }
$env:PSModulePath = $psmodpath -join [io.path]::PathSeparator
$Env:PSModulePathWhich runs like this:

And now we can try again!

Lack of namespace and workaround
One of the downside of using classes in PowerShell modules is that they don’t support namespaces. Our [TheModuleClass] class doesn’t have a fully qualified name. This absence would include the module name it’s defined in. As a result, it makes the class hard to discover and opens the risk of class name conflicts.
Remember when hyper-v and vmware both came up with New-VM cmdlet name?
There have been discussions about that for many years now. I have little hope to see it implemented anytime soon. It’s worth mentioning that PowerShell types are .NET types defined at runtime and they comes with its challenges…
Also, that would be a change not ported back to PowerShell 5.1, so it would have a limited impact anyway.
But with Type Accelerators, we don’t have to limit ourselves with their exact name, they’re aliases anyway…
Nothing is stopping us to export the alias [TheModule.TheModuleClass], and that’s what I often use now. The suffix below is what I usually add to my projects, using Sampler (worthy of a few posts as well!).
#region TypeAccelerator export for classes
# If classes/types are specified here, they won't be fully qualified (careful with conflicts)
$typesToExportAsIs = @(
# 'APIEndpoint'
)
# The type accelerators created will be ModuleName.ClassName (to avoid conflicts with other modules until you use 'using moduleName'
$typesToExportWithNamespace = @(
'TheModuleClass'
)
# inspired from https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_classes?view=powershell-7.5
# Always clobber an existing type accelerator, but
# warn if a type accelerator with the same name exists.
function Get-CurrentModule
{
<#
.SYNOPSIS
This is a Private function to always be able to retrieve the module info even outside
of a function (i.e. PSM1 during module loading)
.DESCRIPTION
This function is only meant to be used from the psm1, hence not exported.
.EXAMPLE
$m = Get-CurrentModule
#>
[OutputType([System.Management.Automation.PSModuleInfo])]
param
()
# Get the current module
$MyInvocation.MyCommand.ScriptBlock.Module
}
# Get the internal TypeAccelerators class to use its static methods.
$typeAcceleratorsClass = [psobject].Assembly.GetType(
'System.Management.Automation.TypeAccelerators'
)
$moduleName = (Get-CurrentModule).Name
$existingTypeAccelerators = $typeAcceleratorsClass::Get
foreach ($typeToExport in @($typesToExportWithNamespace + $typesToExportAsIs))
{
if ($typeToExport -in $typesToExportAsIs)
{
$fullTypeToExport = $TypeToExport
}
else
{
$fullTypeToExport = '{0}.{1}' -f $moduleName,$TypeToExport
}
$type = $TypeToExport -as [System.Type]
if (-not $type)
{
$Message = @(
"Unable to register type accelerator '$fullTypeToExport' for '$typeToExport'"
"Type '$typeToExport' not found."
) -join ' - '
throw [System.Management.Automation.ErrorRecord]::new(
[System.InvalidOperationException]::new($Message),
'TypeAcceleratorTypeNotFound',
[System.Management.Automation.ErrorCategory]::InvalidOperation,
$fullTypeToExport
)
}
else
{
if ($fullTypeToExport -in $existingTypeAccelerators.Keys)
{
$Message = @(
"Overriding type accelerator '$($fullTypeToExport)' with '$($Type.FullName)'"
'Accelerator already exists.'
) -join ' - '
Write-Warning -Message $Message
}
else
{
Write-Verbose -Message "Added type accelerator '$($fullTypeToExport)' for '$($Type.FullName)'."
}
$null = $TypeAcceleratorsClass::Add($fullTypeToExport, $Type)
}
}
# Remove type accelerators when the module is removed.
$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
foreach ($TypeName in $typesToExportWithNamespace)
{
$fullTypeToExport = '{0}.{1}' -f $moduleName,$TypeName
$null = $TypeAcceleratorsClass::Remove($fullTypeToExport)
}
}.GetNewClosure()
#endregion
TypeAccelerator as exported classes for Modules
I’ve been using this for a while now, and I like this approach. I tend to export most types I need with the module name ([ModuleName.ClassName]), and sometimes (rarely) I add a theme or category if I have many classes from that module ([ModuleName.Category.ClassName]).
Obviously it’s not perfect. We don’t get intellisense or autocompletion when writing code with that full name. But, it’s a trade off I’m happy to make for clearer code and easier usage of classes.
The code is pretty generic. You only have to populate a list with the names of the types you’d like to export. I’ll soon add it to Sampler as an option…
With this post out of the way, I an open the door to the next one! We will use this with a custom attribute for PowerShell Universal API endpoints. Stay tuned!
Discover more from SynEdgy
Subscribe to get the latest posts sent to your email.