Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

userDefinedTypes against vars #9364

Open
Chris-Mingay opened this issue Dec 23, 2022 · 11 comments
Open

userDefinedTypes against vars #9364

Chris-Mingay opened this issue Dec 23, 2022 · 11 comments
Labels
enhancement New feature or request Needs: Upvote This issue requires more votes to be considered type system

Comments

@Chris-Mingay
Copy link

Just experimenting with userDefinedTypes now and wondering if it's possible to strongly type vars declared within a bicep script?

I have a list of function definitions that I pass to various modules to do bits and pieces.

Crude example of what I currently have

// main.bicep

param PartOfVnet: bool

var functions = [
  {
    name: 'FunctionOne'
    appScaleLimit: 1,
    serverFarmId: '7528ce31-ddaf-4a6a-91a8-13d763e48eae'
  }
  {
    name: 'FunctionTwo'
    appScaleLimit: 10,
    severFarmId: '53c64ed6-7578-4779-96c0-e92e84933465'
  }
]

module deployFunctions './modules/deployFunctions.bicep' = {
  name: 'deployFunctions ',
  params: {
    functions: functions
  }
}

module deployFunctionOutboundTrafficAssociations './modules/deployFunctionOutboundTrafficAssociations .bicep' = if(PartOfVnet) {
  name: 'deployFunctionOutboundTrafficAssociations '
  params: {
    functions: functions
  }
}

Note that the functions array is not typed.

What I'd like

// main.bicep

type Function = {
  name: string
  appScaleLimit: int
  serverFarmId: string
}

Function[] functions = [
  {
    name: 'FunctionOne'
    appScaleLimit: 1,
    serverFarmId: '7528ce31-ddaf-4a6a-91a8-13d763e48eae'
  }
  {
    name: 'FunctionTwo'
    appScaleLimit: 10,
    severFarmId: '53c64ed6-7578-4779-96c0-e92e84933465'
  }
]

Note that in both code blocks there is a typo in the serverFarmId of "FunctionTwo", I am hoping that in the second block I would be shown that I have an error in my array definition.

@ghost ghost added the Needs: Triage 🔍 label Dec 23, 2022
@Chris-Mingay
Copy link
Author

Sorry I'm well aware this isn't actually an issue, I just went into autopilot for some reason! I'll redo it as a discussion

@jeskew jeskew added the enhancement New feature or request label Dec 28, 2022
@jeskew
Copy link
Contributor

jeskew commented Dec 28, 2022

Reopening as a feature request per the discussion in #9365.

@glen-olsen-poweroffice-no

Is there any work being done on this? The way it works right now using parameters instead of variables only helps me in maybe half of my scenarios. So right now I have a mess where my custom types are (re)used in a few places, and other places I have to just provide object type variables since the values need to reference a resource.

@schaijkw
Copy link

schaijkw commented Feb 7, 2024

@anthony-c-martin Maybe this helps prioritizing
This will also help in combination with functions. When you use an userDefinedType as input parameter, you want to construct a strongly typed input object (with intellisense) instead of inline creating an object:

myFuncInput input = {
  Foo: bar
  Bar: Foo
  FooBar: SomeValue
}
var result = myFunc(input)

instead of:

var result = myFunc({
  Foofoo: invalidproperty
  Bar: Foo
  FooBar: SomeValue
})

@BalassaMarton
Copy link

IMO the following syntax would be more consistent with existing typing features of the language:

type Function = {
  name: string
  appScaleLimit: int
  serverFarmId: string
}

var functions: Function[] = [ /* ... */ ]

@watfordsuzy
Copy link

watfordsuzy commented May 14, 2024

I guess I'm struggling to understand the utility of types if we cannot type variables. Surely this is a miss on my part reading the documentation and not an actual limitation:

@export()
type X = {
   A: string
   B: string
}

// how do we type this?!
@export()
var x = {
  A: '1'
  B: '2'
}

EDIT: for exports this seems to work oddly enough, though I wonder what downstream issues it may cause:

@export()
type X = {
   A: string
   B: string
}

func makeX() X => {
  A: '1'
  B: '2'
}

// how do we type this?!
@export()
var x = makeX()

@jeskew
Copy link
Contributor

jeskew commented May 15, 2024

There are a couple of reasons as to why this isn't supported yet, but the two that are the biggest hurdles for me are:

  1. Types on variables will be (weak) attestations rather than (strong) guarantees
  2. Types on exported variables won't necessarily be used on modules published to a registry.

Types provide strong guarantees at the moment because they are enforced at runtime by the ARM engine. ARM type checking is performed on template and function inputs and outputs, and the check can't be bypassed. Bicep type checking, on the other hand, can be bypassed because the compiler is often working with partial data:

func echo(in string) string => in

param untyped_object object // <-- Bicep never knows the value of a param

var may_fail_at_runtime = echo(untyped_object.property)

Bicep doesn't raise an error on the last line because it has no idea whether untyped_object will have a property named property during a given deployment, much less whether that property's value will be a string. During a deployment, however, the ARM engine is working with concrete values and will enforce that anything passed to the echo function is a string. There's a similar compile-time type check bypass using the any() function.

Because there's no similar check at runtime for variable assignments, typed vars would be a compile-time only feature. Unless we specifically blocked the following, a template author could use the permissive aspects of the type system to "lie" about variable types:

var untyped_object: object = {}

@export()
var property: string = untyped_object.property // <-- importing `property` into another template will cause it to fail at deploy time

@export()
var type_masquerade: string = any(100) // <-- no compiler error, but the value is not a string

There's also nowhere in an ARM JSON template that variable types would be placed. These could be published as template-level metadata (i.e., the same place Bicep publishes descriptions for exported variables), but that would be cementing variable types as never being enforced by ARM.

EDIT: for exports this seems to work oddly enough, though I wonder what downstream issues it may cause:

@export()
type X = {
   A: string
   B: string
}

func makeX() X => {
  A: '1'
  B: '2'
}

// how do we type this?!
@export()
var x = makeX()

There are no downstream issues with this construction. The ARM runtime will validate function results against the declared output type and raise an error if the result does not match the declared type. Any template that imports x will be importing a copy of makeX and then executing it.

@miqm
Copy link
Collaborator

miqm commented Jul 25, 2024

+1 on this too. I sometimes prepare a module where I intend that it will be extended by other team members. Things are not params, as they need to match fixed values in application code. Having ability to hint variables while developing (object of subnets for a vnet module for example) or to have warnings on compile time.

I think it's not required to put information about a variable type into arm schema. Having it in bicep only could be sufficient. Variable type could be imported from module i want to use.
Basically, we need to remember that vars ar not variables - they're in fact constants.

This feature would be only for development time, I don't think we would need it during runtime. Adding it to bicep, even if it will not work in all cases (outputs of functions, etc) it will be not worse than it's now.

Also, i would stick to just assigning types to variables for development purposes only, leaving export decorator as is (on types only)

@miqm
Copy link
Collaborator

miqm commented Jul 26, 2024

As for the design I'd stick to model that we already have for params and outputs, i.e.

type varType = {
  prop: string
  prop2: int
}

var someVar varType = { prop: 'abc', prop2: 123 }

import * as moduleTypes from 'module.bicep'
var modParamVar moduleTypes.paramType = { abc: 'wxy' }

module mod 'module.bicep' = {
  params: {
   modParam: modParamVar
  }
}

var varAnonymousType { abc: string } = { abc: 'xyz' }

although more of a real-life usage would be something like this:

var subnets { *: vnetTypes.subnetType } = {
  snet01: { name: 'snet-001', addressSpace: '172.16.0.0/24' }
  snet02: { name: 'snet-002', addressSpace: '172.16.1.0/24' }
}
import * as vnetTypes from 'vnet.bicep'
module vnet 'vnet.bicep' = {
  params: {
    subnets: map(items(subnets), x => x.value)
  }
}

@BalassaMarton
Copy link

@jeskew I could live with variable typing being only a compile-time check.

@anthony-c-martin
Copy link
Member

anthony-c-martin commented Jul 29, 2024

from discussion on 7/29

Possible options:

  1. Don't represent the type in the generated template at all
  2. Represent variables similar to parameters/outputs in the template ("foo": {"value": .., "type: ..} rather than just "foo": ".." (most likely requires a new language version)
  3. Represent types in template metadata and avoid changing the template schema
  4. Option (3) in the short term, option (2) in the long term

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request Needs: Upvote This issue requires more votes to be considered type system
Projects
Status: Todo
Development

No branches or pull requests

9 participants