In PowerShell 7+, `ValidateSet` provides both input validation and convenient tab completion. I have parameters whose accepted names map to other values, so I would like to define that mapping only once and use it for both validation/completion and lookup.
For functions, an `IValidateSetValuesGenerator` class can read the keys from a shared mapping:
```powershell
$script:optionToConfig = @{
A = 'AAAAA'
B = 'BBBBB'
C = 'CCCCC'
}
class TestValidateSetValuesGenerator : Management.Automation.IValidateSetValuesGenerator {
[string[]] GetValidValues() { return $script:optionToConfig.Keys }
}
function Test([Parameter(Mandatory)][ValidateSet([TestValidateSetValuesGenerator])][string]$Option) {
Write-Host $optionToConfig[$Option]
}
```
However, this approach cannot be used for a script's top-level `param` block because PowerShell requires that block to appear before the class and mapping declarations. What are the best alternatives for retaining tab completion, validation, and a single source of truth in a script file?
3 Answers
A shared configuration object or cross-reference table is another clean option. Store each accepted name together with the values associated with it, then select the property you need at runtime. That avoids duplicating the mapping, although it does not provide `ValidateSet` completion by itself, so you would still need an argument completer if interactive tab completion is important.
Use a custom argument completer for completion and `ValidateScript` for validation. The completer can read the shared mapping, while the validation block checks whether the supplied value is one of its keys:
```powershell
$script:optionToConfig = @{
A = 'AAAAA'
B = 'BBBBB'
C = 'CCCCC'
Da = 'DAAA'
Db = 'DBBBB'
}
param(
[ArgumentCompleter({
param($CommandName, $ParameterName, $WordToComplete, $CommandAst, $FakeBoundParameters)
$script:optionToConfig.Keys |
Where-Object { $_ -like "$WordToComplete*" } |
ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_)
}
})]
[ValidateScript({
if ($_ -notin $script:optionToConfig.Keys) {
throw "Option '$_' is not valid."
}
$true
})]
[string]$Option
)
Write-Host $optionToConfig[$Option]
```
If the same completion logic is needed by several commands, register one shared completer with `Register-ArgumentCompleter` instead of repeating it on every parameter.
`ValidateScript` can handle the validation portion, but it does not automatically provide the completion behavior of `ValidateSet`. Pairing it with `ArgumentCompleter` is therefore the practical choice when the accepted values come from a variable or other runtime data. For more complex parameter behavior, moving the implementation into a compiled or advanced cmdlet may also make the structure cleaner, but it is not required just to solve this mapping problem.
Exactly—the issue with using only `ValidateScript` is that completion must then be implemented separately. Also, pipeline support or adding `CmdletBinding` is not inherently more modern; it depends on how the command is intended to be used.

That pattern is useful for keeping the data centralized, but without a completer it loses the main benefit I am after: being able to type the command and tab through the available constants instead of memorizing them.