Working with User-Defined Types in Bicep
User Defined Types in Bicep
The Bicep team continues to deliver new features and functionality with each release of Bicep. Many of these new features are first released in an experimental state to allow customers to begin testing and provide feedback early in the development process. User-Defined Types are a feature I have been looking forward to for a long time. As of a recent release of Bicep v0.21.1, this feature is now generally available!
What are User-defined Types
User-Define Types are a type
statement that allows us to define custom types in our templates.
They are defined in a similar way as parameters:
1// parameter
2param <parameter-name> <parameter-data-type> = <default-value>
3
4//user define type
5type <user-defined-data-type-name> = <type-expression>
In past versions of Bicep you were limited to supported data types for parameters (string, integer, boolean, arrays, objects). We can now create custom types to better describe the parameters needed for the deployment. User-Defined types only use the primitive literal types (string, integer, boolean). The real power is how you combine these these to create your custom types.
1// A string type array
2type stringArray1 = string[]
3
4// a string with 3 valid options
5type stringEnvironments = 'dev' | 'qa' | 'prod'
6
7// A type can use other user-defined types, Here we combine the previous types into 1
8type parameterObject = {
9 @description('We can also use *some* decorators on our types.')
10 stringArray: stringArray1? //adding a ? makes the property optional when using this type
11 @description('We can also use *some* decorators on our types')
12 environments: stringEnvironments
13}
14
15param parameterObject parameterObject
Using the User-Define Type provides additional intellisense for that type and any other types that were used in that type. .
Why am I excited about this feature?
Writing complex objects or arrays in Bicep today is like a guessing game. I can require or use values or properties in parts of the template and if these are not documented properly, it increases the difficulty for others using that template. Having to hunt through a Bicep template or readme to find out what values are required in an array or an object can be a blocker to adoption of your template by other teammates. Bicep is amazingly good at providing helpful intellisense to avoid this kind of context switching. The major gap had always been parameter objects and arrays. Let's look at how User-Define Types fill this gap.
Examples
Let's start with a simple template for creating tags for a subscription. Every subscription should have a set of tags that are required for various teams and compliance reasons. Here is a sample of what the required parameters looked like before user-defined types:
1// Here is a set of parameters needed to get our required tags
2@description('Required. Environment for deployed resources')
3@allowed(['dev', 'qa', 'prod'])
4param environmentTag string
5
6@description('Required. Project Number for subscription')
7param projectNumberTag string
8
9@description('Required. Are backups required for this subscription')
10@allowed(['True', 'False'])
11param backupRequiredTag string
12
13@description('Optional. backup retention options')
14@allowed(['', '1-Week', '2-Week', '1-Month', '2-Month', '1-Year'])
15param backupRetentionTag string
16
17@description('Optional. What day for backups to run')
18@allowed(['', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'])
19param backupWindowTag string
20
21@description('Optional. Any additional tags required for this subscription')
22param additionalTags object = {}
23
24var requiredSubscriptionTags = {
25 environment: environmentTag
26 projectNumber: projectNumberTag
27 backupRequired: backupRequiredTag
28 backupRetention: backupRetentionTag
29 backupWindow: backupWindowTag
30}
31
32var allsubscriptionTags = union(requiredSubscriptionTags, additionalTags)
From a parameter file, I would need to define all 6 parameters and values. With User-Defined Types we now have more flexibility to define our objects directly.
Here is a User-Define Type alternative:
1type subscriptionTagValues = {
2 @description('Required. Environment for deployed resources')
3 environment: 'dev'| 'qa' | 'prod'
4 @description('Required. Project Number for subscription')
5 projectNumber: string
6 @description('Required. Are backups required for this subscription')
7 backupRequired: 'True' | 'False'
8 @description('Optional. backup retention options')
9 backupRetentiond: ''|'1-Week'|'2-Week'|'1-Month'|'2-Month'|'1-Year'
10 @description('Optional. What day for backups to run')
11 backupWindow: ''|'Monday'|'Tuesday'|'Wednesday'|'Thursday'|'Friday'|'Saturday'|'Sunday'
12 *:string
13}
14
15param subscriptionTags subscriptionTagValues
The Tag Object contains all of our required tags and all the required values and a description of the type property. From our parameter file we get the same intellisense but with a simplified definition of our tag parameter .
An additional benefit of this approach is the ability to add tags that are not defined in the object! The final line of the object, *:string
, allows the object to take additional undefined properties of the type string.
At time of writing, you can only add one type (string, int, boolean) for additional properties at a time. This further simplifies our template parameters by removing additional steps.
Conclusion
User-Define Types help to simplify writing templates by giving us the power to define and document complex parameter objects. No one enjoys having to reverse engineer a template just to implement it. Removing that additional uncertainty of confusion bring template re-usability to the next level. Giving teams the ability to define and describe template parameters without needing to write additional documentation will drive adoption with teammates and co-workers.