Repository: Azure/bicep

Issue #4158: Strong typing for parameters and outputs
Created: 2021-08-26T01:25:53Z
Is your feature request related to a problem? Please describe.
For parameters and outputs the set of possible types that code can declare is very limited (primitives, array, and object). When the type of a module parameter is object you don't get the same type safety guarantees that are normally possible in bicep. Having the ability to specify a more specific type than object would add type-safety for complex parameters and outputs in modules.
Here's a motivating example:
// A rule looks like ....
// {
//   name: 'rule'
//   properties: {
//     frontendPort: 5000
//     protocol: 'Tcp'
//.    ...
//   }
// }

param rule object

resource balancer 'Microsoft.Network/loadBalancers@2020-06-01' = {
  name: 'cool-load-balancer'
  properties: {
    loadBalancingRules: [
      rule
    ]
  }
}
Load balancers can be large and complex. If you want to parameterize a load balancer with modules, you have the choice to either forego type checking, or write a flat list of parameters that get combined into an object.
note: There is already a proposal for strongly-typed resources as parameters/outputs. This is about objects that are not resources.
Describe the solution you'd like
I'd like the ability to specify or infer a more specific type so that the parameters and outputs can be type-checked. I've discussed a few different options with the team, and want to gather more feedback. It's possible that more than one of these solutions are desirable given that they optimize for different scenarios.
Proposal: Refer to types by name
The simplest idea to understand is being able to specify the type using the named definitions that the compiler already knows. These are based on the OpenAPI descriptions used to generate Bicep's type definitions. In this example the type of rule is determined by looking up the provided type string against the resource type definitions.
param rule type 'Microsoft.Network/loadBalancers@2020-06-01#LoadBalancerRule'

resource balancer 'Microsoft.Network/loadBalancers@2020-06-01' = {
  name: 'cool-load-balancer'
  properties: {
    loadBalancingRules: [
      rule
    ]
  }
}
The part after the # is the type name, which is looked up in the context of the provided resource type. Failure to find the specified type would be a warning the failure to find a declared resource type.
To make this work we'd want to provide completion for the part after #. So the user can get completion for the whole string in 3 parts.
note: the syntax shown here is chosen to be similar/consistent with this proposal for type-specifiers for resources.

Pro: This is really simple to understand and consistent with how many languages specify types.
Con: Discoverability might be poor if the names chosen in the OpenAPI are poor. Fortunately these names are also used in SDK generation.
Con: Only applies to named types. For example a string property with an enum of allowed values is not a named type.

Proposal: Add a typeof specifier
This is similar to typeof type operator in TypeScript. In this example that type of the rule parameter is specify by type checking the provided expression.
param rule typeof balancer.properties.loadBalancingRules[0]

resource balancer 'Microsoft.Network/loadBalancers@2020-06-01' = {
  name: 'cool-load-balancer'
  properties: {
    loadBalancingRules: [
      rule
    ]
  }
}
The expression passed to typeof could be any expression (in theory) and could be combined with other type-specifier constructs as necessary. The expression is not evaluated, it is only type-checked.
note: the TypeScript typeof type operator is limited to property access and indexing. We probably need a way to specify that we want the element type of an array. Typescript uses typeof MyArray[number], which would seem foreign in bicep. In this case I filled that in as [*] but it needs more thought.

Pro: This is relatively simple to understand what it does, and could be combined with other constructs.
Pro: Users generally know the properties they need to set so this is slightly more discoverable than the type names, which don't appear in bicep code today.
Pro: Since this refers to a property and not a type, we also have the ability to populate things like documentation or validation constraints.
Con: Might require a special syntax or be confusing when used with the element-type of an array. Likewise doing typeof a resource created in a loop might be confusing compared to other options.
Con: Can refer to types that are not part of the ARM-JSON spec. eg: union of string | int. There's no way to encode this in ARM-JSON today.

Proposal: Type inference via target-typing
This is similar to type inference in where it applies in TypeScript or some functional languages. Similar to (but more complex) target-typed new() in C#. In this example the type rule has its type inferred based on where it appears.
param rule auto

resource balancer 'Microsoft.Network/loadBalancers@2020-06-01' = {
  name: 'cool-load-balancer'
  properties: {
    loadBalancingRules: [
      rule
    ]
  }
}
note: This really optimizes for the use-case where a parameter is used once, or used in the same context. Lots of complex scenarios are possible when type inference gets involved.

Pro: Very terse and easy to adopt in the cases where it applies.
Pro: Since this refers to a property and not a type, we also have the ability to populate things like documentation or validation constraints.
Con: Complex cases arise here when you need to use a parameter in multiple contexts. This could degrade gracefully to any in the worse case.
Con: Can refer to types that are not part of the ARM-JSON spec. eg: union of string | int. There's no way to encode this in ARM-JSON today.

Duplicate issue #7499: Complex parameter handling
Created: 2022-07-06T15:45:56Z
Is your feature request related to a problem? Please describe.
Parameters currently are either a simple type like Boolean, String or Integer or an arbitrary Object type. The object types are often needed for more complex or nested parameters, but cannot be type/content checked internally.
Decorators like @description and @allowed are only available on the root object, not on the keys and @allowed can't validate for keys afaik:
@allowed([
  { test: 123 }
])
@description('Optional. Nested parameters')
param testParam object = { abc: 234 } // Still valid
There is also no intellisense for nested object parameters, which is a real downgrade for the end user when comparing modules vs resources in bicep.
Resources natively provide intellisense and type checking for multiple layers of nested parameters, which we should enable for modules/parameters as well.
Here is an example for a type, where it would make sense to implement the sku options with a sku parameter. Unfortunately, the specifics and limitations for the subfields are so complex that you don't want to provide the user just with a sku object, but with some guidance and type checking. The only way to do it currently is to create a parameter for each subfield and provide the respective @allowed and @description decorators.
@allowed([
  'Basic'
  'GeneralPurpose'
  'MemoryOptimized'
])
@description('Optional. The tier of the particular SKU, e.g. Basic.')
param skuTier string = 'GeneralPurpose'

@allowed([
  2
  4
  8
  16
  32
  64
])
@description('Optional. The scale up/out capacity, representing server\'s compute units.')
param capacity int = 4

@allowed([
  'Gen5'
])
@description('Optional. The family of hardware.')
param skuFamily string = 'Gen5'

@description('Optional. The size code, to be interpreted by resource as appropriate.')
param skuSize string

// Create a sku object from the parameters:

var skuName = '${skuTier == 'Basic' ? 'B' : skuTier == 'GeneralPurpose' ? 'GP' : 'MO'}_${skuFamily}_${capacity}'
var sku: {
    capacity: capacity
    family: skuFamily
    name: skuName
    size: skuSize
    tier: skuTier
  }

resource server 'Microsoft.DBforPostgreSQL/servers@2017-12-01' = {
  name: name
  location: location
  tags: tags
  sku: sku
...
Describe the solution you'd like
I can see multiple ways to do this.

Use the same way as the resource, probably a JSON or Bicep schema file, although even for resources the description or types are often different between ARM/Bicep intellisense and the MS Docs.
Provide some way to nest parameters with decorators for objects. Here with a new key parameter type:

@allowed([
  @allowed([
    2
    4
    8
    16
    32
    64
  ])
  @description('Optional. The scale up/out capacity, representing server\'s compute units.')
  key capacity int = 4

  @allowed([
    'Gen5'
  ])
  @description('Optional. The family of hardware.')
  key skuFamily string = 'Gen5'
])
@description('Optional. The SKU (pricing tier) of the server.')
param sku object = {}

Provide the ability to construct complex parameter objects instead of directly nesting them:

@allowed([
  2
  4
  8
  16
  32
  64
])
@description('Optional. The scale up/out capacity, representing server\'s compute units.')
key/subparam capacity int = 4

@allowed([
  'Gen5'
])
@description('Optional. The family of hardware.')
key/subparam skuFamily string = 'Gen5'

@allowed([
  capacity
  skuFamily
])
@description('Optional. The SKU (pricing tier) of the server.')
param sku object = {}
These are just some options that came to my mind. You probably thought about this already and I would love to hear your ideas on how to handle these.
My motivation comes mainly from building a lot of custom and CARML modules over the last months and the missing validation for nested parameters was one of the main causes for validation issues and unhandled errors.

Closing comment (2022-07-06T19:30:55Z): Closing as dup of #4158. Definitely strong interest in getting this done before v1.0. cc @jeskew as FYI

Thanks for closing the dup, I searched for something similar but didn't find this one issue :)