I'm designing a module and using classes to type-validate my parameters. I noticed that, when attempting to type-validate input parameters, a class with a single-argument constructor appears to act as a type accelerator instead of validating data type.
Example:
Class stack {
$a
$b
stack($inp) {
$this.a = $inp
$this.b = 'anything'
}
}
function foo {
Param(
[stack]$bar
)
$bar
}
PS>foo -bar 'hello'
a b
- -
hello anything
$bar has been type accelerated into an instantiation of stack.
Compare this to the same class with a constructor that takes 2 arguments:
Class stack {
$a
$b
stack($inp,$inp2) {
$this.a = $inp
$this.b = 'anything'
}
}
function foo {
Param(
[stack]$bar
)
$bar
}
PS>foo -bar 'hello'
foo : Cannot process argument transformation on parameter 'bar'. Cannot convert the "hello" value of type "System.String" to type "stack".
Now the class type is correctly validating the input parameter.
I first saw this in PS5.1 on Windows 10, but I just tried it on my private laptop with pwsh 7.2.1 and seems to be the same.
Is there a workaround to this behavior? Is it a bug?
Edit: Well, after further testing, I realized this also happens if I supply 2 input parameters for the constructor with 2 arguments, e.g., foo -bar 'hello' 'world'. So I guess it's probably intended, and I'm doing something wrong. Can I use classes to validate my data types for input parameters? How?
What you're seeing is unrelated to type accelerators, which are simply short alias names for .NET type names; e.g., [regex] is short for [System.Text.RegularExpressions.Regex].
Instead, you're seeing PowerShell's flexible automatic type conversions, which include translating casts (e.g. [stack] ...) and type constraints (ditto, in the context of an assignment or inside a param(...) block) into constructor calls or ::Parse() calls, as explained in this answer.
[stack] class has a (non-type-constrained) single-argument constructor, something like [stack] 'hello' is automatically translated into [stack]::new('hello'), i.e. a constructor call - and that is also what happens when you pass argument 'hello' to a parameter whose type is [stack].I suggest not fighting these automatic conversions, as they are usually helpful.
In the rare event that you do need to ensure that the type of the argument passed is exactly of the type specified in the parameter declaration (or of a derived type), you can use the following technique (using type [datetime] as an example, whose full .NET type name is System.DateTime):
function Foo {
param(
# Ensure that whatever argument is passed is already of type [datetime]
[PSTypeName('System.DateTime')]
$Bar
)
"[$Bar]"
}
Kudos to you for discovering the [PSTypeName()] attribute for this use case.
Without the [PSTypeName(...)] attribute, a call such as Foo 1/1/1970 would work, because the string '1/1/1970' is automatically converted to [datetime] by PowerShell.
With the [PSTypeName(...)] attribute, only an actual [datetime] argument is accepted (or, for types that can be sub-classed, an instance of a type derived from the specified type).
Important: Specify the target type's full .NET type name (e.g. 'System.DateTime' rather than just 'datetime') to target it unambiguously.
classes, their name is the full name (they are not inside a namespace), so in the case of your [stack] class, the attribute would be [PSTypeName('stack')]Any type name is accepted, even if it doesn't refer to an existing .NET type or custom class, and any such non-existent type would require an argument to use a matching virtual ETS (PowerShell's Extended Type System) type name. In fact, supporting such virtual type names is the primary purpose of this attribute.[1] E.g., if [PSTypeName('Bar')] were used, you could pass a custom object with an ETS type name of Bar as follows:
[pscustomobject] @{ PSTypeName = 'Bar'; Baz = 'quux' }
[1] To quote from the linked docs (emphasis added): "This attribute is used to restrict the type name of the parameter, when the type goes beyond the .NET type system."
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With