Repository: rokucommunity/brighterscript
Pull request #946 (2023-11-18 22:15:01)

Interface optional properties
![image](https://github.com/rokucommunity/brighterscript/assets/810290/439853a7-e0da-44c4-a4c2-08967088bd6a)


rokucommunity-bot[bot]: Hey there! I just built a new temporary npm package based on 9c80b6cc6ae420f68bd216785a6972993d9f1a51. You can download it [here](https://github.com/rokucommunity/brighterscript/releases/download/v0.0.0-packages/brighterscript-0.66.0-alpha.7-interface-optional-properties.20231120195531.tgz) or install it by running the following command: 
```bash
npm install https://github.com/rokucommunity/brighterscript/releases/download/v0.0.0-packages/brighterscript-0.66.0-alpha.7-interface-optional-properties.20231120195531.tgz
```
rokucommunity-bot[bot]: Hey there! I just built a new temporary npm package based on 460e8c5cf5f98a5752cb0e3fd5995a51b0f31daa. You can download it [here](https://github.com/rokucommunity/brighterscript/releases/download/v0.0.0-packages/brighterscript-0.66.0-alpha.7-interface-optional-properties.20231120195829.tgz) or install it by running the following command: 
```bash
npm install https://github.com/rokucommunity/brighterscript/releases/download/v0.0.0-packages/brighterscript-0.66.0-alpha.7-interface-optional-properties.20231120195829.tgz
```
rokucommunity-bot[bot]: Hey there! I just built a new temporary npm package based on c3d93c5ec035e1f1691b60714abb110d2c0cf5ee. You can download it [here](https://github.com/rokucommunity/brighterscript/releases/download/v0.0.0-packages/brighterscript-0.66.0-alpha.7-interface-optional-properties.20231120200224.tgz) or install it by running the following command: 
```bash
npm install https://github.com/rokucommunity/brighterscript/releases/download/v0.0.0-packages/brighterscript-0.66.0-alpha.7-interface-optional-properties.20231120200224.tgz
```
rokucommunity-bot[bot]: Hey there! I just built a new temporary npm package based on e6b7997d8e48f55d72684ff117b718dd3a0e8e59. You can download it [here](https://github.com/rokucommunity/brighterscript/releases/download/v0.0.0-packages/brighterscript-0.66.0-alpha.7-interface-optional-properties.20231121115709.tgz) or install it by running the following command: 
```bash
npm install https://github.com/rokucommunity/brighterscript/releases/download/v0.0.0-packages/brighterscript-0.66.0-alpha.7-interface-optional-properties.20231121115709.tgz
```
rokucommunity-bot[bot]: Hey there! I just built a new temporary npm package based on ce85a65cc834ff9e7f5cbd3c027d739d84f7a7fa. You can download it [here](https://github.com/rokucommunity/brighterscript/releases/download/v0.0.0-packages/brighterscript-0.66.0-alpha.7-interface-optional-properties.20231121135504.tgz) or install it by running the following command: 
```bash
npm install https://github.com/rokucommunity/brighterscript/releases/download/v0.0.0-packages/brighterscript-0.66.0-alpha.7-interface-optional-properties.20231121135504.tgz
```

Patch:
diff --git a/.github/workflows/create-package.yml b/.github/workflows/create-package.yml
index 196c3a38..6605f6df 100644
--- a/.github/workflows/create-package.yml
+++ b/.github/workflows/create-package.yml
@@ -34,8 +34,6 @@ jobs:
           echo "ARTIFACT_URL=$ARTIFACT_URL" >> $GITHUB_ENV
 
       - run: npm ci
-      - run: npm config set ignore-scripts true
-      - run: npm run build
       - run: npm version "$BUILD_VERSION" --no-git-tag-version
       - run: npm pack
 
diff --git a/docs/classes.md b/docs/classes.md
index 4e2ff8ef..1b9f7c30 100644
--- a/docs/classes.md
+++ b/docs/classes.md
@@ -3,6 +3,7 @@
 Traditional BrightScript supports object creation (see [roAssociativeArray](https://developer.roku.com/docs/references/brightscript/components/roassociativearray.md)), but these can get a bit complicated at times, are generally created in a factory function. These objects are also difficult to track from a type-checking perspective, as their shape can morph over time. In BrighterScript, you can declare actual classes, and compile them into pure BrightScript which can be used on any Roku device.
 
 ## Classes
+
 Here's an example of a simple BrighterScript class
 
 ```vb
@@ -35,11 +36,12 @@ function Animal()
 end function
 ```
 
-Notice that there are two functions created in the transpiled code for the `Animal` class. At runtime, BrighterScript classes are built in two steps in order to support class inheritance. The first step uses the `__ClassName_Build()` method to create the skeleton structure of the class. Then the class's constructor function will be run. Child classes will call the parent's `__ParentClassName_Build()` method, then rename overridden  methods, and then call the child's constructor (without calling the parent constructor). Take a look at the transpiled output of the other examples below for more information on this.
-
+Notice that there are two functions created in the transpiled code for the `Animal` class. At runtime, BrighterScript classes are built in two steps in order to support class inheritance. The first step uses the `__ClassName_Build()` method to create the skeleton structure of the class. Then the class's constructor function will be run. Child classes will call the parent's `__ParentClassName_Build()` method, then rename overridden methods, and then call the child's constructor (without calling the parent constructor). Take a look at the transpiled output of the other examples below for more information on this.
 
 ## Inheritance
+
 In BrighterScript, we can use patterns common to other object-oriented languages such as using inheritance to create a new class based off of an existing class.
+
 ```BrighterScript
 class Animal
     sub new(name as string)
@@ -81,6 +83,7 @@ sub Main()
     '> Waddling...\nDewey moved 2 meters\nFell over...I'm new at this
 end sub
 ```
+
 <details>
   <summary>View the transpiled BrightScript code</summary>
 
@@ -150,10 +153,11 @@ sub Main()
     '> Waddling...\nDewey moved 2 meters\nFell over...I'm new at this
 end sub
 ```
-</details>
 
+</details>
 
 ## Constructor function
+
 The constructor function for a class is called `new`.
 
 ```vb
@@ -185,9 +189,11 @@ function Duck(name as string)
     return instance
 end function
 ```
+
 </details>
 
 ### Constructor function with inheritance
+
 When constructing a child that inherits from a base class, the first call in the child's constructor must be a call to the parent's constructor
 
 ```vb
@@ -243,9 +249,11 @@ function BabyDuck(name as string, age as integer)
     return instance
 end function
 ```
+
 </details>
 
 ## Overrides
+
 Child classes can override methods on parent classes. In this example, the `BabyDuck.Eat()` method completely overrides the parent method. Note: the `override` keyword is mandatory, and you will get a compile error if it is not included in the child class and there is a matching method on the base class. Also, you will get a compile error if the override keyword is present in a child class, but that method doesn't exist in the parent class.
 
 ```vb
@@ -298,9 +306,11 @@ function BabyDuck()
     return instance
 end function
 ```
+
 </details>
 
 ### Calling parent method from child
+
 You can also call the original methods on the base class from within an overridden method on a child class.
 
 ```vb
@@ -318,6 +328,7 @@ class BabyDuck extends Duck
 end class
 
 ```
+
 <details>
   <summary>View the transpiled BrightScript code</summary>
 
@@ -355,9 +366,11 @@ function BabyDuck()
     return instance
 end function
 ```
+
 </details>
 
 ## Public by default
+
 Class fields and methods are public by default, which aligns with the general BrightScript approach that "everything is public".
 
 ```vb
@@ -379,6 +392,7 @@ class Person
     end sub
 end class
 ```
+
 <details>
   <summary>View the transpiled BrightScript code</summary>
 
@@ -407,11 +421,13 @@ function Person()
     return instance
 end function
 ```
+
 </details>
 
 You will get compile-time errors whenever you access private members of a class from outside the class. However, be aware that this is only a compile-time restriction. At runtime, all members are public.
 
 ## Dynamic type by default
+
 You can specify a type for class fields and a return type for methods. However, this is entirely optional. All fields and methods have a default type of `dynamic`. However, BrighterScript will attempt to infer the type from usage. Take this for example:
 
 ```vb
@@ -428,6 +444,7 @@ class Person
     end function
 end class
 ```
+
 <details>
   <summary>View the transpiled BrightScript code</summary>
 
@@ -452,9 +469,11 @@ function Person()
     return instance
 end function
 ```
+
 </details>
 
 ## Property initialization
+
 Like most other object-oriented classes, you can initialze a property with a default value.
 
 ```BrighterScript
@@ -463,6 +482,7 @@ class Duck
     hasChildren = true
 end class
 ```
+
 <details>
   <summary>View the transpiled BrightScript code</summary>
 
@@ -481,16 +501,18 @@ function Duck()
     return instance
 end function
 ```
-</details>
 
+</details>
 
 ## Usage
+
 In order to use a class, you need to construct one. Based on our person class above, you can create a new person like this:
 
 ```vb
 donald = new Person("Donald")
 daisy = new Person("Daisy")
 ```
+
 <details>
   <summary>View the transpiled BrightScript code</summary>
 
@@ -498,10 +520,11 @@ daisy = new Person("Daisy")
 donald = Person("Donald")
 daisy = Person("Daisy")
 ```
-</details>
 
+</details>
 
 ## Namespaces
+
 Classes can also be contained within a namespace. At runtime, all namespace periods are replaced with underscores.
 
 ```BrighterScript
@@ -513,6 +536,7 @@ namespace Vertibrates.Birds
     end class
 end namespace
 ```
+
 <details>
   <summary>View the transpiled BrightScript code</summary>
 
@@ -542,10 +566,11 @@ function Vertibrates_Birds_Duck()
     return instance
 end function
 ```
-</details>
 
+</details>
 
 ## Instance binding
+
 As you would expect, methods attached to a class will have their `m` assigned to that class instance whenever they are invoked through the object. (i.e. `someClassInstance.doSomething()`. However, keep in mind that functions themselves retain no direct knowledge of what object they are bound to.
 
 This is no different than the way plain BrightScript functions and objects interact, but it was worth mentioning that classes cannot mitigate this fundamental runtime limitation.
@@ -576,6 +601,7 @@ sub main()
     sayHello() ' prints "Main method"
 end sub
 ```
+
 <details>
   <summary>View the transpiled BrightScript code</summary>
 
@@ -607,4 +633,19 @@ sub main()
     sayHello() ' prints "Main method"
 end sub
 ```
+
 </details>
+
+## Optional fields
+
+Classes can include optional fields, denoted with the keyword `optional` before the name. If a class has an optional field, it signifies that the value of the field may be `invalid`. Optional fields in a class do not change how the class or its members are validated.
+
+```brighterscript
+class Video
+    url as string
+    length as float
+    optional subtitleUrl as string
+    optional rating as string
+    optional genre as string[]
+end class
+```
diff --git a/docs/interfaces.md b/docs/interfaces.md
index 996bda9c..90fb52a7 100644
--- a/docs/interfaces.md
+++ b/docs/interfaces.md
@@ -1,7 +1,9 @@
 # Interfaces
+
 Interfaces are a way to describe properties and methods on an object.
 
 ## Simple example
+
 Interfaces can have both properties and methods.
 
 ```BrighterScript
@@ -24,7 +26,9 @@ end sub
 ```
 
 ## Use in functions
+
 Interfaces can be used as function parameter types and return types. These types will be converted into `dynamic` when the project is transpiled.
+
 ```brighterscript
 interface Dog
     name as string
@@ -46,9 +50,11 @@ sub walkThePuppy(puppy as dynamic) as dynamic
     return puppy
 end sub
 ```
+
 </details>
 
 ## Namespaces
+
 Interfaces can also be defined inside namespaces
 
 ```BrighterScript
@@ -73,7 +79,9 @@ end sub
 ```
 
 ## Advanced types
+
 Interface member types can be defined as any valid brighterscript type, and even reference themselves.
+
 ```brighterscript
 interface Dog
     owner as Human
@@ -91,12 +99,15 @@ end interface
 ```BrightScript
 
 ```
+
 </details>
 
 </details>
 
 ## Methods
+
 Interfaces can describe complex methods as well
+
 ```brighterscript
 interface Dog
     sub barkAt(nemesis as Cat)
@@ -107,5 +118,30 @@ end interface
   <summary>View the transpiled BrightScript code</summary>
 
 ```BrightScript
+
+```
+
+</details>
+
+## Optional members
+
+Interfaces can include optional members. Optional members are denoted with the keyword `optional` before the name of fields, or before the `function` or `sub` keyword for methods. Interfaces with optional members will not incur a validation diagnostic if an object that is missing that member is passed to it, yet optional members will be included in completion results, and if referenced, they are inferred to be their declared type.
+
+```brighterscript
+interface Video
+    url as string
+    length as float
+    optional subtitleUrl as string
+    optional rating as string
+    optional genre as string[]
+end interface
+```
+
+<details>
+  <summary>View the transpiled BrightScript code</summary>
+
+```BrightScript
+
 ```
+
 </details>
diff --git a/src/AstValidationSegmenter.ts b/src/AstValidationSegmenter.ts
index e7177606..aad13233 100644
--- a/src/AstValidationSegmenter.ts
+++ b/src/AstValidationSegmenter.ts
@@ -54,7 +54,7 @@ export class AstValidationSegmenter {
         const options: GetTypeOptions = { flags: flag, onlyCacheResolvedTypes: true, typeChain: typeChain };
 
         const nodeType = expression.getType(options);
-        if (!nodeType.isResolvable()) {
+        if (!nodeType?.isResolvable()) {
             let symbolsSet: Set<UnresolvedSymbol>;
             if (!assignedSymbols?.has(typeChain[0].name.toLowerCase())) {
                 if (!this.unresolvedSegmentsSymbols.has(segment)) {
diff --git a/src/SymbolTable.ts b/src/SymbolTable.ts
index e052d8e2..9ab0b900 100644
--- a/src/SymbolTable.ts
+++ b/src/SymbolTable.ts
@@ -8,7 +8,8 @@ import { isAnyReferenceType, isReferenceType } from './astUtils/reflection';
 
 export enum SymbolTypeFlag {
     runtime = 1,
-    typetime = 2
+    typetime = 2,
+    optional = 4
 }
 
 /**
@@ -198,7 +199,7 @@ export class SymbolTable implements SymbolTypeGetter {
         if (!symbolArray) {
             return undefined;
         }
-        return symbolArray?.map(symbol => ({ type: symbol.type, data: symbol.data }));
+        return symbolArray?.map(symbol => ({ type: symbol.type, data: symbol.data, flags: symbol.flags }));
     }
 
     getSymbolType(name: string, options: GetSymbolTypeOptions): BscType {
@@ -207,9 +208,11 @@ export class SymbolTable implements SymbolTypeGetter {
         let doSetCache = !resolvedType;
         const originalIsReferenceType = isAnyReferenceType(resolvedType);
         let data = {} as ExtraSymbolData;
+        let foundFlags: SymbolTypeFlag;
         if (!resolvedType || originalIsReferenceType) {
             const symbolTypes = this.getSymbolTypes(name, options);
             data = symbolTypes?.[0]?.data;
+            foundFlags = symbolTypes?.[0].flags;
             resolvedType = getUniqueType(symbolTypes?.map(symbol => symbol.type), SymbolTable.unionTypeFactory);
         }
         if (!resolvedType && options.fullName && options.tableProvider) {
@@ -219,11 +222,12 @@ export class SymbolTable implements SymbolTypeGetter {
         const newNonReferenceType = originalIsReferenceType && !isAnyReferenceType(resolvedType);
         doSetCache = doSetCache && (options.onlyCacheResolvedTypes ? !resolvedTypeIsReference : true);
         if (doSetCache || newNonReferenceType) {
-            this.setCachedType(name, { type: resolvedType, data: data }, options);
+            this.setCachedType(name, { type: resolvedType, data: data, flags: foundFlags }, options);
         }
         if (options.data) {
             options.data.definingNode = data?.definingNode;
             options.data.description = data?.description;
+            options.data.flags = foundFlags;
         }
         return resolvedType;
     }
@@ -379,4 +383,5 @@ export interface GetSymbolTypeOptions extends GetTypeOptions {
 export interface TypeCacheEntry {
     type: BscType;
     data?: ExtraSymbolData;
+    flags?: SymbolTypeFlag;
 }
diff --git a/src/bscPlugin/completions/CompletionsProcessor.spec.ts b/src/bscPlugin/completions/CompletionsProcessor.spec.ts
index 08d274a1..b7fded8d 100644
--- a/src/bscPlugin/completions/CompletionsProcessor.spec.ts
+++ b/src/bscPlugin/completions/CompletionsProcessor.spec.ts
@@ -1662,4 +1662,70 @@ describe('CompletionsProcessor', () => {
 
     });
 
+    describe('interfaces', () => {
+
+        it('finds members of interfaces', () => {
+            program.setFile('source/main.bs', `
+                sub foo(thing as SomeInterface )
+                    print thing.
+                end sub
+
+
+                interface SomeInterface
+                    name as string
+                    data
+                    function doStuff()
+                end interface
+            `);
+            program.validate();
+            //  print thing.|
+            const completions = program.getCompletions('source/main.bs', util.createPosition(2, 33));
+            expectCompletionsIncludes(completions, [{
+                label: 'name',
+                kind: CompletionItemKind.Field
+            }]);
+            expectCompletionsIncludes(completions, [{
+                label: 'data',
+                kind: CompletionItemKind.Field
+            }]);
+            expectCompletionsIncludes(completions, [{
+                label: 'doStuff',
+                kind: CompletionItemKind.Method
+            }]);
+
+        });
+
+        it('finds optional members of interfaces', () => {
+            program.setFile('source/main.bs', `
+                sub foo(thing as SomeInterface )
+                    print thing.
+                end sub
+
+
+                interface SomeInterface
+                    optional name as string
+                    optional data
+                    optional function doStuff()
+                end interface
+            `);
+            program.validate();
+            //  print thing.|
+            const completions = program.getCompletions('source/main.bs', util.createPosition(2, 33));
+            expectCompletionsIncludes(completions, [{
+                label: 'name',
+                kind: CompletionItemKind.Field
+            }]);
+            expectCompletionsIncludes(completions, [{
+                label: 'data',
+                kind: CompletionItemKind.Field
+            }]);
+            expectCompletionsIncludes(completions, [{
+                label: 'doStuff',
+                kind: CompletionItemKind.Method
+            }]);
+
+        });
+
+    });
+
 });
diff --git a/src/bscPlugin/completions/CompletionsProcessor.ts b/src/bscPlugin/completions/CompletionsProcessor.ts
index cbed955d..af11db10 100644
--- a/src/bscPlugin/completions/CompletionsProcessor.ts
+++ b/src/bscPlugin/completions/CompletionsProcessor.ts
@@ -1,5 +1,5 @@
 import { isBrsFile, isCallableType, isClassType, isComponentType, isConstStatement, isEnumMemberType, isEnumType, isInterfaceType, isMethodStatement, isNamespaceType, isNativeType, isXmlFile, isXmlScope } from '../../astUtils/reflection';
-import type { ExtraSymbolData, FileReference, ProvideCompletionsEvent } from '../../interfaces';
+import type { FileReference, ProvideCompletionsEvent } from '../../interfaces';
 import type { File } from '../../files/File';
 import { DeclarableTypes, Keywords, TokenKind } from '../../lexer/TokenKind';
 import type { XmlScope } from '../../XmlScope';
@@ -18,6 +18,7 @@ import type { BscType } from '../../types';
 import type { AstNode } from '../../parser/AstNode';
 import type { FunctionStatement } from '../../parser/Statement';
 import type { Token } from '../../lexer/Token';
+import { createIdentifier } from '../../astUtils/creators';
 
 
 export class CompletionsProcessor {
@@ -258,7 +259,7 @@ export class CompletionsProcessor {
             const sortText = symbol.data?.completionPriority ? 'z'.repeat(symbol.data?.completionPriority) + symbol.name : undefined;
             return {
                 label: symbol.name,
-                kind: this.getCompletionKindFromType(symbol.type, symbol.data, areMembers),
+                kind: this.getCompletionKindFromSymbol(symbol, areMembers),
                 detail: symbol?.type?.toString(),
                 documentation: this.getDocumentation(symbol),
                 sortText: sortText
@@ -274,7 +275,9 @@ export class CompletionsProcessor {
     }
 
 
-    private getCompletionKindFromType(type: BscType, extraData?: ExtraSymbolData, areMembers = false) {
+    private getCompletionKindFromSymbol(symbol: BscSymbol, areMembers = false) {
+        const type = symbol?.type;
+        const extraData = symbol?.data;
         if (isConstStatement(extraData?.definingNode)) {
             return CompletionItemKind.Constant;
         } else if (isClassType(type)) {
@@ -289,10 +292,16 @@ export class CompletionsProcessor {
             return CompletionItemKind.EnumMember;
         } else if (isNamespaceType(type)) {
             return CompletionItemKind.Module;
-        } else if (isNativeType(type)) {
+        }
+        if (areMembers) {
+            return CompletionItemKind.Field;
+        }
+        const tokenIdentifier = util.tokenToBscType(createIdentifier(symbol.name));
+        if (isNativeType(tokenIdentifier)) {
             return CompletionItemKind.Keyword;
+
         }
-        return areMembers ? CompletionItemKind.Field : CompletionItemKind.Variable;
+        return CompletionItemKind.Variable;
     }
 
 
diff --git a/src/bscPlugin/hover/HoverProcessor.ts b/src/bscPlugin/hover/HoverProcessor.ts
index baf92a19..29f8b15c 100644
--- a/src/bscPlugin/hover/HoverProcessor.ts
+++ b/src/bscPlugin/hover/HoverProcessor.ts
@@ -16,6 +16,7 @@ import type { BscType } from '../../types';
 
 const fence = (code: string) => util.mdFence(code, 'brightscript');
 
+
 export class HoverProcessor {
     public constructor(
         public event: ProvideHoverEvent
@@ -165,7 +166,14 @@ export class HoverProcessor {
                     }
                     hoverContent = fence(`${variableName}${exprType.toString()}`);
                 }
-
+                const modifiers = [];
+                // eslint-disable-next-line no-bitwise
+                if (extraData?.flags && extraData?.flags & SymbolTypeFlag.optional) {
+                    modifiers.push(TokenKind.Optional);
+                }
+                if (modifiers.length > 0) {
+                    hoverContent += `\n*(${modifiers.join(', ').toLowerCase()})*`;
+                }
 
                 if (extraData.description) {
                     hoverContent = this.buildContentsWithDocsFromDescription(hoverContent, extraData.description);
diff --git a/src/bscPlugin/transpile/BrsFileTranspileProcessor.ts b/src/bscPlugin/transpile/BrsFileTranspileProcessor.ts
index 17a733e6..17d60f1c 100644
--- a/src/bscPlugin/transpile/BrsFileTranspileProcessor.ts
+++ b/src/bscPlugin/transpile/BrsFileTranspileProcessor.ts
@@ -66,8 +66,11 @@ export class BrsFilePreTranspileProcessor {
             };
         }
         //assume we've been given the enum.member syntax, so pop the member and try again
-        const parts = name.split('.');
+        const parts = name.toLowerCase().split('.');
         const memberName = parts.pop();
+        if (containingNamespace) {
+            parts.unshift(containingNamespace.toLowerCase());
+        }
         result = scope?.getEnumMap().get(parts.join('.'));
         if (result) {
             const value = result.item.getMemberValue(memberName);
diff --git a/src/bscPlugin/validation/ScopeValidator.spec.ts b/src/bscPlugin/validation/ScopeValidator.spec.ts
index c7d4cfe5..631bcb03 100644
--- a/src/bscPlugin/validation/ScopeValidator.spec.ts
+++ b/src/bscPlugin/validation/ScopeValidator.spec.ts
@@ -7,6 +7,7 @@ import type { TypeCompatibilityData } from '../../interfaces';
 import { IntegerType } from '../../types/IntegerType';
 import { StringType } from '../../types/StringType';
 import type { BrsFile } from '../../files/BrsFile';
+import { FloatType } from '../../types';
 
 describe('ScopeValidator', () => {
 
@@ -1257,6 +1258,153 @@ describe('ScopeValidator', () => {
                 expectZeroDiagnostics(program);
             });
         });
+
+        describe('interface with optional properties', () => {
+
+            it('allows using interfaces with optional props', () => {
+                program.setFile('source/util.bs', `
+                    function takesIFace(iface as MyIFace) as string
+                        if invalid <> iface.name
+                            return iface.name
+                        else if invalid <> iface.data
+                            return FormatJson(iface.data)
+                        end if
+                        return "no"
+                    end function
+
+                    sub doStuff(iface as MyIFace)
+                        print takesIFace(iface)
+                    end sub
+
+                    interface MyIFace
+                        optional name as string
+                        optional data
+                    end interface
+                `);
+                program.validate();
+                expectZeroDiagnostics(program);
+            });
+
+            it('allows using passing AAs with missing optional properties', () => {
+                program.setFile('source/util.bs', `
+                    function takesIFace(iface as MyIFace) as string
+                        if invalid <> iface.name
+                            return iface.name
+                        else if invalid <> iface.data
+                            return FormatJson(iface.data)
+                        end if
+                        return "no"
+                    end function
+
+                    sub doStuff(iface as MyIFace)
+                        print takesIFace({name: "Hello"})
+                    end sub
+
+                    interface MyIFace
+                        optional name as string
+                        optional data
+                    end interface
+                `);
+                program.validate();
+                expectZeroDiagnostics(program);
+            });
+
+            it('disallows using AAs with bad types for optional properties', () => {
+                program.setFile('source/util.bs', `
+                    function takesIFace(iface as MyIFace) as string
+                        if invalid <> iface.name
+                            return iface.name
+                        else if invalid <> iface.data
+                            return FormatJson(iface.data)
+                        end if
+                        return "no"
+                    end function
+
+                    sub doStuff(iface as MyIFace)
+                        print takesIFace({name: 3.14})
+                    end sub
+
+                    interface MyIFace
+                        optional name as string
+                        optional data
+                    end interface
+                `);
+                program.validate();
+                expectDiagnostics(program, [
+                    DiagnosticMessages.argumentTypeMismatch('roAssociativeArray', 'MyIFace', {
+                        fieldMismatches: [{ name: 'name', expectedType: StringType.instance, actualType: FloatType.instance }]
+                    }).message
+                ]);
+            });
+
+            it('disallows passing classes with bad types for optional properties', () => {
+                program.setFile('source/util.bs', `
+                    function takesIFace(iface as MyIFace) as string
+                        if invalid <> iface.name
+                            return iface.name
+                        else if invalid <> iface.data
+                            return FormatJson(iface.data)
+                        end if
+                        return "no"
+                    end function
+
+                    sub doStuff(iface as MyIFace)
+                        k = new MyKlass()
+                        print takesIFace(k)
+                    end sub
+
+                    interface MyIFace
+                        optional name as string
+                        optional data
+                    end interface
+
+                    class MyKlass
+                        name as float
+                    end class
+                `);
+                program.validate();
+                expectDiagnostics(program, [
+                    DiagnosticMessages.argumentTypeMismatch('MyKlass', 'MyIFace', {
+                        fieldMismatches: [{ name: 'name', expectedType: StringType.instance, actualType: FloatType.instance }]
+                    }).message
+                ]);
+            });
+
+            it('allows passing classes as args for interfaces with optional properties', () => {
+                program.setFile('source/util.bs', `
+                    function takesIFace(iface as MyIFace) as string
+                        if invalid <> iface.name
+                            return iface.name
+                        else if invalid <> iface.data
+                            return FormatJson(iface.data)
+                        end if
+                        return "no"
+                    end function
+
+                    sub doStuff(iface as MyIFace)
+                        k = new MyKlass()
+                        k2 = new MyKlass2()
+                        print takesIFace(k)
+                        print takesIFace(k2)
+                    end sub
+
+                    interface MyIFace
+                        optional name as string
+                        optional data
+                    end interface
+
+                    class MyKlass
+                        data = {}
+                    end class
+
+                    class MyKlass2
+                        data = "test"
+                    end class
+                `);
+                program.validate();
+                expectZeroDiagnostics(program);
+            });
+        });
     });
 
     describe('cannotFindName', () => {
@@ -1285,6 +1433,23 @@ describe('ScopeValidator', () => {
                 DiagnosticMessages.cannotFindName('expected').message
             ]);
         });
+
+        it('does not have a diagnostic for using a variable the result of an assignment with unresolved value', () => {
+            program.setFile('source/util.bs', `
+                sub doStuff()
+                    myValue = UndeclaredValue
+                    if myValue > 0
+                        print "hello"
+                    end if
+                end sub
+            `);
+            program.validate();
+            //should have only 1 error - cannot find "UndeclaredValue"
+            expectDiagnostics(program, [
+                DiagnosticMessages.cannotFindName('UndeclaredValue').message
+            ]);
+        });
+
     });
 
     describe('returnTypeMismatch', () => {
diff --git a/src/bscPlugin/validation/ScopeValidator.ts b/src/bscPlugin/validation/ScopeValidator.ts
index abd33ada..b430e9e5 100644
--- a/src/bscPlugin/validation/ScopeValidator.ts
+++ b/src/bscPlugin/validation/ScopeValidator.ts
@@ -4,7 +4,7 @@ import { Cache } from '../../Cache';
 import { DiagnosticMessages } from '../../DiagnosticMessages';
 import type { BrsFile } from '../../files/BrsFile';
 import { DiagnosticOrigin } from '../../interfaces';
-import type { BsDiagnostic, BsDiagnosticWithOrigin, OnScopeValidateEvent, TypeChainEntry, TypeCompatibilityData } from '../../interfaces';
+import type { BsDiagnostic, BsDiagnosticWithOrigin, ExtraSymbolData, OnScopeValidateEvent, TypeChainEntry, TypeCompatibilityData } from '../../interfaces';
 import { SymbolTypeFlag } from '../../SymbolTable';
 import type { AssignmentStatement, DottedSetStatement, EnumStatement, ReturnStatement } from '../../parser/Statement';
 import util from '../../util';
@@ -19,6 +19,7 @@ import { createVisitor } from '../../astUtils/visitors';
 import type { BscType } from '../../types';
 import type { File } from '../../files/File';
 import { InsideSegmentWalkMode } from '../../AstValidationSegmenter';
+import { TokenKind } from '../../lexer/TokenKind';
 
 /**
  * The lower-case names of all platform-included scenegraph nodes
@@ -127,12 +128,19 @@ export class ScopeValidator {
     /**
      * If this is the lhs of an assignment, we don't need to flag it as unresolved
      */
-    private ignoreUnresolvedAssignmentLHS(expression: Expression, exprType: BscType) {
+    private ignoreUnresolvedAssignmentLHS(expression: Expression, exprType: BscType, definingNode?: AstNode) {
         if (!isVariableExpression(expression)) {
             return false;
         }
-        const assignmentAncestor: AssignmentStatement = expression?.findAncestor(isAssignmentStatement);
-        return assignmentAncestor?.name === expression?.name && isUnionType(exprType); // the left hand side is not a union, which means it was never assigned
+        let assignmentAncestor: AssignmentStatement;
+        if (isAssignmentStatement(definingNode) && definingNode.equals.kind === TokenKind.Equal) {
+            // this symbol was defined in a "normal" assignment (eg. not a compound assignment)
+            assignmentAncestor = definingNode;
+            return assignmentAncestor?.name?.text.toLowerCase() === expression?.name?.text.toLowerCase();
+        } else {
+            assignmentAncestor = expression?.findAncestor(isAssignmentStatement);
+        }
+        return assignmentAncestor?.name === expression?.name && isUnionType(exprType);
     }
 
     /**
@@ -494,12 +502,14 @@ export class ScopeValidator {
         // Do a complete type check on all DottedGet and Variable expressions
         // this will create a diagnostic if an invalid member is accessed
         const typeChain: TypeChainEntry[] = [];
+        const typeData = {} as ExtraSymbolData;
         let exprType = expression.getType({
             flags: symbolType,
-            typeChain: typeChain
+            typeChain: typeChain,
+            data: typeData
         });
 
-        const shouldIgnoreLHS = this.ignoreUnresolvedAssignmentLHS(expression, exprType);
+        const shouldIgnoreLHS = this.ignoreUnresolvedAssignmentLHS(expression, exprType, typeData?.definingNode);
 
         if (!this.isTypeKnown(exprType) && !shouldIgnoreLHS) {
             if (expression.getType({ flags: oppositeSymbolType })?.isResolvable()) {
diff --git a/src/files/BrsFile.Class.spec.ts b/src/files/BrsFile.Class.spec.ts
index 491620f0..d2fd4eeb 100644
--- a/src/files/BrsFile.Class.spec.ts
+++ b/src/files/BrsFile.Class.spec.ts
@@ -73,6 +73,29 @@ describe('BrsFile BrighterScript classes', () => {
         ]);
     });
 
+    it('allows class named `optional`', () => {
+        program.setFile('source/main.bs', `
+            class optional
+                thing = 1
+            end class
+        `);
+        program.validate();
+        expectZeroDiagnostics(program);
+    });
+
+    it('supports optional fields', () => {
+        program.setFile('source/main.bs', `
+            class Movie
+                name as string
+                optional subtitles as string
+                public optional isRepeatEnabled as boolean
+                private optional wasPlayed
+            end class
+        `);
+        program.validate();
+        expectZeroDiagnostics(program);
+    });
+
     it('access modifier is option for override', () => {
         let file = program.setFile<BrsFile>({ src: `${rootDir}/source/main.bs`, dest: 'source/main.bs' }, `
             class Animal
@@ -893,6 +916,83 @@ describe('BrsFile BrighterScript classes', () => {
                 end function
             `, 'trim', 'source/main.bs');
         });
+
+        it('works with enums as field initial values inside a namespace', async () => {
+            await testTranspile(`
+                namespace MyNS
+                    class HasEnumKlass
+                        enumValue = MyEnum.A
+                    end class
+
+                    enum MyEnum
+                        A = "A"
+                        B = "B"
+                    end enum
+                end namespace
+            `, `
+                function __MyNS_HasEnumKlass_builder()
+                    instance = {}
+                    instance.new = sub()
+                        m.enumValue = "A"
+                    end sub
+                    return instance
+                end function
+                function MyNS_HasEnumKlass()
+                    instance = __MyNS_HasEnumKlass_builder()
+                    instance.new()
+                    return instance
+                end function
+            `, 'trim', 'source/main.bs');
+        });
+
+        it('allows enums as super args inside a namespace', async () => {
+            await testTranspile(`
+                namespace MyNS
+                    class SubKlass extends SuperKlass
+                        sub new()
+                            super(MyEnum.B)
+                        end sub
+                    end class
+
+                    class SuperKlass
+                        sub new(enumVal as MyEnum)
+                            print enumVal
+                        end sub
+                    end class
+
+                    enum MyEnum
+                        A = "A"
+                        B = "B"
+                    end enum
+                end namespace
+            `, `
+                function __MyNS_SubKlass_builder()
+                    instance = __MyNS_SuperKlass_builder()
+                    instance.super0_new = instance.new
+                    instance.new = sub()
+                        m.super0_new("B")
+                    end sub
+                    return instance
+                end function
+                function MyNS_SubKlass()
+                    instance = __MyNS_SubKlass_builder()
+                    instance.new()
+                    return instance
+                end function
+                function __MyNS_SuperKlass_builder()
+                    instance = {}
+                    instance.new = sub(enumVal as dynamic)
+                        print enumVal
+                    end sub
+                    return instance
+                end function
+                function MyNS_SuperKlass(enumVal as dynamic)
+                    instance = __MyNS_SuperKlass_builder()
+                    instance.new(enumVal)
+                    return instance
+                end function
+            `, 'trim', 'source/main.bs');
+        });
     });
 
     it('detects using `new` keyword on non-classes', () => {
diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts
index 315ff495..b487fd7e 100644
--- a/src/files/BrsFile.spec.ts
+++ b/src/files/BrsFile.spec.ts
@@ -134,6 +134,23 @@ describe('BrsFile', () => {
         }]);
     });
 
+    it('allows namespaces with the name `optional`', () => {
+        program.setFile('source/main.bs', `
+            namespace optional
+                namespace optional
+                end namespace
+            end namespace
+            namespace alpha
+                namespace optional
+                end namespace
+            end namespace
+            namespace alpha.beta.optional
+            end namespace
+        `);
+        program.validate();
+        expectZeroDiagnostics(program);
+    });
+
     it('flags enums used as variables', () => {
         program.setFile('source/main.bs', `
             enum Foo
@@ -3021,6 +3038,7 @@ describe('BrsFile', () => {
                 range: util.createRange(5, 26, 5, 33)
             }]);
         });
+
         it('returns enum locations', () => {
             const file = program.setFile<BrsFile>('source/main.bs', `
                 sub main()
@@ -3040,6 +3058,78 @@ describe('BrsFile', () => {
             }]);
         });
 
+        it('returns interface location', () => {
+            const file = program.setFile<BrsFile>('source/main.bs', `
+                sub test(selectedMovie as Movie)
+                    print selectedMovie
+                end sub
+                interface Movie
+                    url as string
+                end interface
+            `);
+            program.validate();
+            // sub test(selectedMovie as Mo|vie)
+            expect(program.getDefinition(file.srcPath, Position.create(1, 44))).to.eql([{
+                uri: URI.file(file.srcPath).toString(),
+                range: util.createRange(4, 26, 4, 31)
+            }]);
+        });
+
+        it('returns namespaced interface location', () => {
+            const file = program.setFile<BrsFile>('source/main.bs', `
+                sub test(selectedMovie as interfaces.Movie)
+                    print selectedMovie
+                end sub
+                namespace interfaces
+                    interface Movie
+                        url as string
+                    end interface
+                end namespace
+            `);
+            program.validate();
+            //sub test(selectedMovie as interfaces.Mo|vie)
+            expect(program.getDefinition(file.srcPath, Position.create(1, 55))).to.eql([{
+                uri: URI.file(file.srcPath).toString(),
+                range: util.createRange(5, 30, 5, 35)
+            }]);
+        });
+
+        it('returns class location', () => {
+            const file = program.setFile<BrsFile>('source/main.bs', `
+                sub test(selectedMovie as Movie)
+                    print selectedMovie
+                end sub
+                class Movie
+                    url as string
+                end class
+            `);
+            program.validate();
+            //sub test(selectedMovie as Mo|vie)
+            expect(program.getDefinition(file.srcPath, Position.create(1, 44))).to.eql([{
+                uri: URI.file(file.srcPath).toString(),
+                range: util.createRange(4, 22, 4, 27)
+            }]);
+        });
+
+        it('returns namespaced class location', () => {
+            const file = program.setFile<BrsFile>('source/main.bs', `
+                sub test(selectedMovie as classes.Movie)
+                    print selectedMovie
+                end sub
+                namespace classes
+                    class Movie
+                        url as string
+                    end class
+                end namespace
+            `);
+            program.validate();
+            //sub test(selectedMovie as classes.Mo|vie)
+            expect(program.getDefinition(file.srcPath, Position.create(1, 52))).to.eql([{
+                uri: URI.file(file.srcPath).toString(),
+                range: util.createRange(5, 26, 5, 31)
+            }]);
+        });
+
         it('does not crash on nulls', () => {
             const file = program.setFile<BrsFile>('source/main.bs', `
                 sub main()
diff --git a/src/files/BrsFile.ts b/src/files/BrsFile.ts
index cef392bf..552f5993 100644
--- a/src/files/BrsFile.ts
+++ b/src/files/BrsFile.ts
@@ -1080,7 +1080,8 @@ export class BrsFile implements File {
                 );
                 return results;
             }
-            if (isDottedGetExpression(expression)) {
+
+            if (isDottedGetExpression(expression) || isVariableExpression(expression)) {
 
                 const enumLink = scope.getEnumFileLink(fullName, containingNamespace);
                 if (enumLink) {
@@ -1102,6 +1103,28 @@ export class BrsFile implements File {
                     );
                     return results;
                 }
+
+                const interfaceFileLink = scope.getInterfaceFileLink(fullName, containingNamespace);
+                if (interfaceFileLink) {
+                    results.push(
+                        util.createLocation(
+                            URI.file(interfaceFileLink.file.srcPath).toString(),
+                            interfaceFileLink.item.tokens.name.range
+                        )
+                    );
+                    return results;
+                }
+
+                const classFileLink = scope.getClassFileLink(fullName, containingNamespace);
+                if (classFileLink) {
+                    results.push(
+                        util.createLocation(
+                            URI.file(classFileLink.file.srcPath).toString(),
+                            classFileLink.item.name.range
+                        )
+                    );
+                    return results;
+                }
             }
         }
 
diff --git a/src/interfaces.ts b/src/interfaces.ts
index aab1c697..41a540fa 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -773,6 +773,7 @@ export interface ExtraSymbolData {
     definingNode?: AstNode;
     description?: string;
     completionPriority?: number; // the higher the number, the lower the priority
+    flags?: SymbolTypeFlag;
 }
 
 export interface GetTypeOptions {
diff --git a/src/lexer/TokenKind.ts b/src/lexer/TokenKind.ts
index 4e05605d..e08fd4fc 100644
--- a/src/lexer/TokenKind.ts
+++ b/src/lexer/TokenKind.ts
@@ -155,6 +155,7 @@ export enum TokenKind {
     Public = 'Public',
     Protected = 'Protected',
     Private = 'Private',
+    Optional = 'Optional',
     As = 'As',
     New = 'New',
     Override = 'Override',
@@ -301,6 +302,7 @@ export const Keywords: Record<string, TokenKind> = {
     private: TokenKind.Private,
     new: TokenKind.New,
     override: TokenKind.Override,
+    optional: TokenKind.Optional,
     namespace: TokenKind.Namespace,
     endnamespace: TokenKind.EndNamespace,
     'end namespace': TokenKind.EndNamespace,
@@ -427,6 +429,7 @@ export const AllowedProperties = [
     TokenKind.Class,
     TokenKind.New,
     TokenKind.Override,
+    TokenKind.Optional,
     TokenKind.Namespace,
     TokenKind.EndNamespace,
     TokenKind.Import,
@@ -471,6 +474,7 @@ export const AllowedLocalIdentifiers = [
     TokenKind.Class,
     TokenKind.New,
     TokenKind.Override,
+    TokenKind.Optional,
     TokenKind.Namespace,
     TokenKind.EndNamespace,
     TokenKind.Import,
diff --git a/src/parser/Parser.Class.spec.ts b/src/parser/Parser.Class.spec.ts
index 15aacf66..22d043f2 100644
--- a/src/parser/Parser.Class.spec.ts
+++ b/src/parser/Parser.Class.spec.ts
@@ -7,8 +7,10 @@ import type { FunctionStatement, AssignmentStatement, FieldStatement } from './S
 import { ClassStatement } from './Statement';
 import { NewExpression } from './Expression';
 import { StringType } from '../types/StringType';
-import { expectDiagnosticsIncludes } from '../testHelpers.spec';
+import { expectDiagnosticsIncludes, expectZeroDiagnostics } from '../testHelpers.spec';
 import { SymbolTypeFlag } from '../SymbolTable';
+import { isClassStatement } from '../astUtils/reflection';
+import { parse } from './Parser.spec';
 
 describe('parser class', () => {
     it('throws exception when used in brightscript scope', () => {
@@ -413,4 +415,111 @@ describe('parser class', () => {
             expect(diagnostics[0]?.message).to.not.exist;
         });
     });
+
+    describe('optional members', () => {
+        it('allows optional fields', () => {
+            let { statements, diagnostics } = parse(`
+                class HasOptional
+                    optional name as string
+                    optional height
+                end class
+            `, ParseMode.BrighterScript);
+            expectZeroDiagnostics(diagnostics);
+            expect(statements.length).to.eq(1);
+            expect(isClassStatement(statements[0])).to.be.true;
+            const klass = statements[0] as ClassStatement;
+            klass.fields.forEach(f => expect(f.isOptional).to.be.true);
+            const klassType = klass.getType({ flags: SymbolTypeFlag.typetime });
+            // eslint-disable-next-line no-bitwise
+            klassType.getMemberTable().getAllSymbols(SymbolTypeFlag.runtime).forEach(sym => expect(sym.flags & SymbolTypeFlag.optional).to.eq(SymbolTypeFlag.optional));
+        });
+
+        it('allows fields named optional', () => {
+            let { statements, diagnostics } = parse(`
+                class IsJustOptional
+                    optional
+                    someThingElse
+                end class
+            `, ParseMode.BrighterScript);
+            expectZeroDiagnostics(diagnostics);
+            expect(statements.length).to.eq(1);
+            expect(isClassStatement(statements[0])).to.be.true;
+            const klass = statements[0] as ClassStatement;
+            klass.fields.forEach(f => expect(f.isOptional).to.be.false);
+            const klassType = klass.getType({ flags: SymbolTypeFlag.typetime });
+            const klassMembers = klassType.getMemberTable().getAllSymbols(SymbolTypeFlag.runtime);
+            expect(klassMembers.length).to.eq(2);
+            // eslint-disable-next-line no-bitwise
+            klassMembers.forEach(sym => expect(sym.flags & SymbolTypeFlag.optional).to.eq(0));
+        });
+
+        it('allows typed fields named optional', () => {
+            let { statements, diagnostics } = parse(`
+                class IsJustOptional
+                    optional as string
+                end class
+            `, ParseMode.BrighterScript);
+            expectZeroDiagnostics(diagnostics);
+            expect(statements.length).to.eq(1);
+            expect(isClassStatement(statements[0])).to.be.true;
+            const klass = statements[0] as ClassStatement;
+            klass.fields.forEach(f => expect(f.isOptional).to.be.false);
+            const klassType = klass.getType({ flags: SymbolTypeFlag.typetime });
+            const klassMembers = klassType.getMemberTable().getAllSymbols(SymbolTypeFlag.runtime);
+            expect(klassMembers.length).to.eq(1);
+            // eslint-disable-next-line no-bitwise
+            klassMembers.forEach(sym => expect(sym.flags & SymbolTypeFlag.optional).to.eq(0));
+        });
+
+        it('allows fields named optional that are also optional', () => {
+            let { statements, diagnostics } = parse(`
+                class IsJustOptional
+                    optional optional
+                end class
+            `, ParseMode.BrighterScript);
+            expectZeroDiagnostics(diagnostics);
+            expect(statements.length).to.eq(1);
+            expect(isClassStatement(statements[0])).to.be.true;
+            const klass = statements[0] as ClassStatement;
+            klass.fields.forEach(f => expect(f.isOptional).to.be.true);
+            const klassType = klass.getType({ flags: SymbolTypeFlag.typetime });
+            const klassMembers = klassType.getMemberTable().getAllSymbols(SymbolTypeFlag.runtime);
+            expect(klassMembers.length).to.eq(1);
+            // eslint-disable-next-line no-bitwise
+            klassMembers.forEach(sym => expect(sym.flags & SymbolTypeFlag.optional).to.eq(SymbolTypeFlag.optional));
+        });
+
+        it('disallows optional methods', () => {
+            let { statements, diagnostics } = parse(`
+                class HasOptional
+
+                    optional function getValue() as boolean
+                        return false
+                    end function
+                end class
+            `, ParseMode.BrighterScript);
+            expectDiagnosticsIncludes(diagnostics, [
+                DiagnosticMessages.expectedNewlineOrColon().message
+            ]);
+            expect(statements.length).to.eq(1);
+        });
+
+        it('allows fields named `as` that are also optional', () => {
+            let { statements, diagnostics } = parse(`
+                class IsJustOptional
+                    optional as
+                end class
+            `, ParseMode.BrighterScript);
+            expectZeroDiagnostics(diagnostics);
+            expect(statements.length).to.eq(1);
+            expect(isClassStatement(statements[0])).to.be.true;
+            const klass = statements[0] as ClassStatement;
+            klass.fields.forEach(f => expect(f.isOptional).to.be.true);
+            const klassType = klass.getType({ flags: SymbolTypeFlag.typetime });
+            const klassMembers = klassType.getMemberTable().getAllSymbols(SymbolTypeFlag.runtime);
+            expect(klassMembers.length).to.eq(1);
+            // eslint-disable-next-line no-bitwise
+            klassMembers.forEach(sym => expect(sym.flags & SymbolTypeFlag.optional).to.eq(SymbolTypeFlag.optional));
+        });
+    });
 });
diff --git a/src/parser/Parser.spec.ts b/src/parser/Parser.spec.ts
index 46f4b966..65e34b6c 100644
--- a/src/parser/Parser.spec.ts
+++ b/src/parser/Parser.spec.ts
@@ -4,7 +4,7 @@ import { ReservedWords, TokenKind } from '../lexer/TokenKind';
 import type { AAMemberExpression, BinaryExpression, TypeCastExpression, UnaryExpression } from './Expression';
 import { TernaryExpression, NewExpression, IndexedGetExpression, DottedGetExpression, XmlAttributeGetExpression, CallfuncExpression, AnnotationExpression, CallExpression, FunctionExpression, VariableExpression } from './Expression';
 import { Parser, ParseMode } from './Parser';
-import type { AssignmentStatement, ClassStatement, ReturnStatement } from './Statement';
+import type { AssignmentStatement, ClassStatement, InterfaceStatement, ReturnStatement } from './Statement';
 import { PrintStatement, FunctionStatement, NamespaceStatement, ImportStatement } from './Statement';
 import { Range } from 'vscode-languageserver';
 import { DiagnosticMessages } from '../DiagnosticMessages';
@@ -1631,6 +1631,161 @@ describe('parser', () => {
 
     });
 
+    describe('interfaces', () => {
+
+        it('allows fields and methods', () => {
+            let { statements, diagnostics } = parse(`
+                interface SomeIFace
+                    name as string
+                    height as integer
+                    function getValue(thing as float) as object
+                    function getMe() as SomeIFace
+                    sub noop()
+                end interface
+            `, ParseMode.BrighterScript);
+            expectZeroDiagnostics(diagnostics);
+            expect(statements.length).to.eq(1);
+            expect(isInterfaceStatement(statements[0])).to.be.true;
+        });
+
+        it('allows untyped fields', () => {
+            let { statements, diagnostics } = parse(`
+                interface HasUntyped
+                    name
+                end interface
+            `, ParseMode.BrighterScript);
+            expectZeroDiagnostics(diagnostics);
+            expect(statements.length).to.eq(1);
+            expect(isInterfaceStatement(statements[0])).to.be.true;
+        });
+
+        it('allows optional fields', () => {
+            let { statements, diagnostics } = parse(`
+                interface HasOptional
+                    optional name as string
+                    optional height
+                end interface
+            `, ParseMode.BrighterScript);
+            expectZeroDiagnostics(diagnostics);
+            expect(statements.length).to.eq(1);
+            expect(isInterfaceStatement(statements[0])).to.be.true;
+            const iface = statements[0] as InterfaceStatement;
+            iface.fields.forEach(f => expect(f.isOptional).to.be.true);
+            const ifaceType = iface.getType({ flags: SymbolTypeFlag.typetime });
+            // eslint-disable-next-line no-bitwise
+            ifaceType.getMemberTable().getAllSymbols(SymbolTypeFlag.runtime).forEach(sym => expect(sym.flags & SymbolTypeFlag.optional).to.eq(SymbolTypeFlag.optional));
+        });
+
+        it('allows fields named optional', () => {
+            let { statements, diagnostics } = parse(`
+                interface IsJustOptional
+                    optional
+                    someThingElse
+                end interface
+            `, ParseMode.BrighterScript);
+            expectZeroDiagnostics(diagnostics);
+            expect(statements.length).to.eq(1);
+            expect(isInterfaceStatement(statements[0])).to.be.true;
+            const iface = statements[0] as InterfaceStatement;
+            iface.fields.forEach(f => expect(f.isOptional).to.be.false);
+            const ifaceType = iface.getType({ flags: SymbolTypeFlag.typetime });
+            const iFaceMembers = ifaceType.getMemberTable().getAllSymbols(SymbolTypeFlag.runtime);
+            expect(iFaceMembers.length).to.eq(2);
+            // eslint-disable-next-line no-bitwise
+            iFaceMembers.forEach(sym => expect(sym.flags & SymbolTypeFlag.optional).to.eq(0));
+        });
+
+        it('allows fields named optional that are also optional', () => {
+            let { statements, diagnostics } = parse(`
+                interface IsJustOptional
+                    optional optional
+                end interface
+            `, ParseMode.BrighterScript);
+            expectZeroDiagnostics(diagnostics);
+            expect(statements.length).to.eq(1);
+            expect(isInterfaceStatement(statements[0])).to.be.true;
+            const iface = statements[0] as InterfaceStatement;
+            iface.fields.forEach(f => expect(f.isOptional).to.be.true);
+            const ifaceType = iface.getType({ flags: SymbolTypeFlag.typetime });
+            const iFaceMembers = ifaceType.getMemberTable().getAllSymbols(SymbolTypeFlag.runtime);
+            expect(iFaceMembers.length).to.eq(1);
+            // eslint-disable-next-line no-bitwise
+            iFaceMembers.forEach(sym => expect(sym.flags & SymbolTypeFlag.optional).to.eq(SymbolTypeFlag.optional));
+        });
+
+        it('allows optional methods', () => {
+            let { statements, diagnostics } = parse(`
+                interface HasOptional
+                    optional function getValue() as boolean
+                    optional sub noop()
+                    optional function process()
+                end interface
+            `, ParseMode.BrighterScript);
+            expectZeroDiagnostics(diagnostics);
+            expect(statements.length).to.eq(1);
+            expect(isInterfaceStatement(statements[0])).to.be.true;
+            const iface = statements[0] as InterfaceStatement;
+            iface.methods.forEach(m => expect(m.isOptional).to.equal(true));
+            const ifaceType = iface.getType({ flags: SymbolTypeFlag.typetime });
+            // eslint-disable-next-line no-bitwise
+            ifaceType.getMemberTable().getAllSymbols(SymbolTypeFlag.runtime).forEach(sym => expect(sym.flags & SymbolTypeFlag.optional).to.eq(SymbolTypeFlag.optional));
+        });
+
+        it('allows fields named `as` that are also optional', () => {
+            let { statements, diagnostics } = parse(`
+                interface IsJustOptional
+                    optional as
+                end interface
+            `, ParseMode.BrighterScript);
+            expectZeroDiagnostics(diagnostics);
+            expect(statements.length).to.eq(1);
+            expect(isInterfaceStatement(statements[0])).to.be.true;
+            const iface = statements[0] as InterfaceStatement;
+            iface.fields.forEach(f => expect(f.isOptional).to.be.true);
+            const ifaceType = iface.getType({ flags: SymbolTypeFlag.typetime });
+            const iFaceMembers = ifaceType.getMemberTable().getAllSymbols(SymbolTypeFlag.runtime);
+            expect(iFaceMembers.length).to.eq(1);
+            // eslint-disable-next-line no-bitwise
+            iFaceMembers.forEach(sym => expect(sym.flags & SymbolTypeFlag.optional).to.eq(SymbolTypeFlag.optional));
+        });
+
+        it('allows fields named `as` that are also typed', () => {
+            let { statements, diagnostics } = parse(`
+                interface IsJustOptional
+                    optional as as string
+                end interface
+            `, ParseMode.BrighterScript);
+            expectZeroDiagnostics(diagnostics);
+            expect(statements.length).to.eq(1);
+            expect(isInterfaceStatement(statements[0])).to.be.true;
+            const iface = statements[0] as InterfaceStatement;
+            iface.fields.forEach(f => expect(f.isOptional).to.be.true);
+            const ifaceType = iface.getType({ flags: SymbolTypeFlag.typetime });
+            const iFaceMembers = ifaceType.getMemberTable().getAllSymbols(SymbolTypeFlag.runtime);
+            expect(iFaceMembers.length).to.eq(1);
+            // eslint-disable-next-line no-bitwise
+            iFaceMembers.forEach(sym => expect(sym.flags & SymbolTypeFlag.optional).to.eq(SymbolTypeFlag.optional));
+        });
+
+        it('allows fields named `optional` that are also typed', () => {
+            let { statements, diagnostics } = parse(`
+                interface IsJustOptional
+                    optional as string
+                end interface
+            `, ParseMode.BrighterScript);
+            expectZeroDiagnostics(diagnostics);
+            expect(statements.length).to.eq(1);
+            expect(isInterfaceStatement(statements[0])).to.be.true;
+            const iface = statements[0] as InterfaceStatement;
+            iface.fields.forEach(f => expect(f.isOptional).to.be.false);
+            const ifaceType = iface.getType({ flags: SymbolTypeFlag.typetime });
+            const iFaceMembers = ifaceType.getMemberTable().getAllSymbols(SymbolTypeFlag.runtime);
+            expect(iFaceMembers.length).to.eq(1);
+            // eslint-disable-next-line no-bitwise
+            iFaceMembers.forEach(sym => expect(sym.flags & SymbolTypeFlag.optional).to.eq(0));
+        });
+    });
+
     describe('leadingTrivia', () => {
         it('gets leading trivia from functions', () => {
             let { statements } = parse(`
diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts
index 2486c9c3..d3a98d9f 100644
--- a/src/parser/Parser.ts
+++ b/src/parser/Parser.ts
@@ -394,10 +394,14 @@ export class Parser {
     /**
      * Create a new InterfaceMethodStatement. This should only be called from within `interfaceDeclaration`
      */
-    private interfaceFieldStatement() {
+    private interfaceFieldStatement(optionalKeyword?: Token) {
         const name = this.identifier(...AllowedProperties);
-        const [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
-        return new InterfaceFieldStatement(name, asToken, typeExpression);
+        let asToken;
+        let typeExpression;
+        if (this.check(TokenKind.As)) {
+            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
+        }
+        return new InterfaceFieldStatement(optionalKeyword, name, asToken, typeExpression);
     }
 
     private consumeAsTokenAndTypeExpression(): [Token, TypeExpression] {
@@ -427,10 +431,10 @@ export class Parser {
     /**
      * Create a new InterfaceMethodStatement. This should only be called from within `interfaceDeclaration()`
      */
-    private interfaceMethodStatement() {
+    private interfaceMethodStatement(optionalKeyword?: Token) {
         const functionType = this.advance();
         const name = this.identifier(...AllowedProperties);
-        const leftParen = this.consumeToken(TokenKind.LeftParen);
+        const leftParen = this.consume(DiagnosticMessages.expectedToken(TokenKind.LeftParen), TokenKind.LeftParen);
 
         let params = [] as FunctionParameterExpression[];
         if (!this.check(TokenKind.RightParen)) {
@@ -455,6 +459,7 @@ export class Parser {
         }
 
         return new InterfaceMethodStatement(
+            optionalKeyword,
             functionType,
             name,
             leftParen,
@@ -501,14 +506,19 @@ export class Parser {
                 if (this.check(TokenKind.At)) {
                     this.annotationExpression();
                 }
-
+                const optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
                 //fields
-                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkNext(TokenKind.As)) {
+                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkAnyNext(TokenKind.As, TokenKind.Newline, TokenKind.Comment)) {
+                    decl = this.interfaceFieldStatement(optionalKeyword);
+                    //field with name = 'optional'
+                } else if (optionalKeyword && this.checkAny(TokenKind.As, TokenKind.Newline, TokenKind.Comment)) {
+                    //rewind one place, so that 'optional' is the field name
+                    this.current--;
                     decl = this.interfaceFieldStatement();
 
                     //methods (function/sub keyword followed by opening paren)
-                } else if (this.checkAny(TokenKind.Function, TokenKind.Sub) && this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
-                    decl = this.interfaceMethodStatement();
+                } else if (this.checkAny(TokenKind.Function, TokenKind.Sub) && this.checkAnyNext(TokenKind.Identifier, ...AllowedProperties)) {
+                    decl = this.interfaceMethodStatement(optionalKeyword);
 
                     //comments
                 } else if (this.check(TokenKind.Comment)) {
@@ -562,7 +572,7 @@ export class Parser {
             TokenKind.Enum
         );
 
-        result.tokens.name = this.tryIdentifier();
+        result.tokens.name = this.tryIdentifier(...this.allowedLocalIdentifiers);
 
         this.consumeStatementSeparators();
         //gather up all members
@@ -666,7 +676,6 @@ export class Parser {
                 if (this.peek().text.toLowerCase() === 'override') {
                     overrideKeyword = this.advance();
                 }
-
                 //methods (function/sub keyword OR identifier followed by opening paren)
                 if (this.checkAny(TokenKind.Function, TokenKind.Sub) || (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkNext(TokenKind.LeftParen))) {
                     const funcDeclaration = this.functionDeclaration(false, false);
@@ -746,11 +755,38 @@ export class Parser {
     }
 
     private fieldDeclaration(accessModifier: Token | null) {
+
+        let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
+
+        if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
+            if (this.check(TokenKind.As)) {
+                if (this.checkAnyNext(TokenKind.Comment, TokenKind.Newline)) {
+                    // as <EOL>
+                    // `as` is the field name
+                } else if (this.checkNext(TokenKind.As)) {
+                    //  as as ____
+                    // first `as` is the field name
+                } else if (optionalKeyword) {
+                    // optional as ____
+                    // optional is the field name, `as` starts type
+                    // rewind current token
+                    optionalKeyword = null;
+                    this.current--;
+                }
+            }
+        } else {
+            // no name after `optional` ... optional is the name
+            // rewind current token
+            optionalKeyword = null;
+            this.current--;
+        }
+
         let name = this.consume(
             DiagnosticMessages.expectedClassFieldIdentifier(),
             TokenKind.Identifier,
             ...AllowedProperties
         ) as Identifier;
+
         let asToken: Token;
         let fieldTypeExpression: TypeExpression;
         //look for `as SOME_TYPE`
@@ -768,6 +804,7 @@ export class Parser {
 
         return new FieldStatement(
             accessModifier,
+            optionalKeyword,
             name,
             asToken,
             fieldTypeExpression,
diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts
index 2d7175a6..ed1b035f 100644
--- a/src/parser/Statement.ts
+++ b/src/parser/Statement.ts
@@ -1518,11 +1518,13 @@ export class InterfaceStatement extends Statement implements TypedefProvider {
         const resultType = new InterfaceType(this.getName(ParseMode.BrighterScript), superIface);
         for (const statement of this.methods) {
             const memberType = statement?.getType({ ...options, typeChain: undefined }); // no typechain info needed
-            resultType.addMember(statement?.tokens.name?.text, { definingNode: statement }, memberType, SymbolTypeFlag.runtime);
+            const flag = statement.isOptional ? SymbolTypeFlag.runtime | SymbolTypeFlag.optional : SymbolTypeFlag.runtime;
+            resultType.addMember(statement?.tokens.name?.text, { definingNode: statement }, memberType, flag);
         }
         for (const statement of this.fields) {
             const memberType = statement?.getType({ ...options, typeChain: undefined }); // no typechain info needed
-            resultType.addMember(statement?.tokens.name?.text, { definingNode: statement }, memberType, SymbolTypeFlag.runtime);
+            const flag = statement.isOptional ? SymbolTypeFlag.runtime | SymbolTypeFlag.optional : SymbolTypeFlag.runtime;
+            resultType.addMember(statement?.tokens.name?.text, { definingNode: statement }, memberType, flag);
         }
         options.typeChain?.push(new TypeChainEntry(this.getName(ParseMode.BrighterScript), resultType, options.flags, this.range));
         return resultType;
@@ -1534,14 +1536,17 @@ export class InterfaceFieldStatement extends Statement implements TypedefProvide
         throw new Error('Method not implemented.');
     }
     constructor(
+        optionalToken: Token,
         nameToken: Identifier,
         asToken: Token,
         public typeExpression?: TypeExpression
     ) {
         super();
         this.tokens.name = nameToken;
+        this.tokens.optional = optionalToken;
         this.tokens.as = asToken;
         this.range = util.createBoundingRange(
+            this.tokens.optional,
             this.tokens.name,
             this.tokens.as,
             this.typeExpression
@@ -1553,18 +1558,23 @@ export class InterfaceFieldStatement extends Statement implements TypedefProvide
     public range: Range;
 
     public tokens = {} as {
+        optional: Token;
         name: Identifier;
         as: Token;
     };
 
     public getLeadingTrivia(): Token[] {
-        return util.concatAnnotationLeadingTrivia(this, this.tokens.name.leadingTrivia);
+        return util.concatAnnotationLeadingTrivia(this, this.tokens.optional?.leadingTrivia ?? this.tokens.name.leadingTrivia);
     }
 
     public get name() {
         return this.tokens.name.text;
     }
 
+    public get isOptional() {
+        return !!this.tokens.optional;
+    }
+
     walk(visitor: WalkVisitor, options: WalkOptions) {
         if (options.walkMode & InternalWalkMode.walkExpressions) {
             walk(this, 'typeExpression', visitor, options);
@@ -1580,10 +1590,19 @@ export class InterfaceFieldStatement extends Statement implements TypedefProvide
                 state.indent()
             );
         }
+        if (this.isOptional) {
+            result.push(
+                this.tokens.optional.text,
+                ' '
+            );
+        }
 
         result.push(
             this.tokens.name.text
         );
+        if (this.tokens.optional) {
+            result.push(this.tokens.optional.text);
+        }
         if (this.typeExpression) {
             result.push(
                 ' as ',
@@ -1606,6 +1625,7 @@ export class InterfaceMethodStatement extends Statement implements TypedefProvid
         throw new Error('Method not implemented.');
     }
     constructor(
+        optionalToken: Token,
         functionTypeToken: Token,
         nameToken: Identifier,
         leftParen: Token,
@@ -1615,6 +1635,7 @@ export class InterfaceMethodStatement extends Statement implements TypedefProvid
         public returnTypeExpression?: TypeExpression
     ) {
         super();
+        this.tokens.optional = optionalToken;
         this.tokens.functionType = functionTypeToken;
         this.tokens.name = nameToken;
         this.tokens.leftParen = leftParen;
@@ -1626,6 +1647,7 @@ export class InterfaceMethodStatement extends Statement implements TypedefProvid
 
     public get range() {
         return util.createBoundingRange(
+            this.tokens.optional,
             this.tokens.functionType,
             this.tokens.name,
             this.tokens.leftParen,
@@ -1643,6 +1665,7 @@ export class InterfaceMethodStatement extends Statement implements TypedefProvid
     }
 
     public tokens = {} as {
+        optional: Token;
         functionType: Token;
         name: Identifier;
         leftParen: Token;
@@ -1650,8 +1673,12 @@ export class InterfaceMethodStatement extends Statement implements TypedefProvid
         as: Token;
     };
 
+    public get isOptional() {
+        return !!this.tokens.optional;
+    }
+
     public getLeadingTrivia(): Token[] {
-        return util.concatAnnotationLeadingTrivia(this, this.tokens.functionType.leadingTrivia);
+        return util.concatAnnotationLeadingTrivia(this, this.tokens.optional?.leadingTrivia ?? this.tokens.functionType.leadingTrivia);
     }
 
     walk(visitor: WalkVisitor, options: WalkOptions) {
@@ -1669,12 +1696,17 @@ export class InterfaceMethodStatement extends Statement implements TypedefProvid
                 state.indent()
             );
         }
-
+        if (this.isOptional) {
+            result.push(
+                this.tokens.optional.text,
+                ' '
+            );
+        }
         result.push(
             this.tokens.functionType.text,
             ' ',
             this.tokens.name.text,
-            '('
+            this.tokens.leftParen.text
         );
         const params = this.params ?? [];
         for (let i = 0; i < params.length; i++) {
@@ -1691,7 +1723,7 @@ export class InterfaceMethodStatement extends Statement implements TypedefProvid
             }
         }
         result.push(
-            ')'
+            this.tokens.rightParen.text
         );
         if (this.returnTypeExpression) {
             result.push(
@@ -2120,11 +2152,13 @@ export class ClassStatement extends Statement implements TypedefProvider {
 
         for (const statement of this.methods) {
             const funcType = statement?.func.getType({ ...options, typeChain: undefined }); //no typechain needed
-            resultType.addMember(statement?.name?.text, { definingNode: statement }, funcType, SymbolTypeFlag.runtime);
+            const flag = SymbolTypeFlag.runtime;
+            resultType.addMember(statement?.name?.text, { definingNode: statement }, funcType, flag);
         }
         for (const statement of this.fields) {
             const fieldType = statement.getType({ ...options, typeChain: undefined }); //no typechain needed
-            resultType.addMember(statement?.name?.text, { definingNode: statement }, fieldType, SymbolTypeFlag.runtime);
+            const flag = statement.isOptional ? SymbolTypeFlag.runtime | SymbolTypeFlag.optional : SymbolTypeFlag.runtime;
+            resultType.addMember(statement?.name?.text, { definingNode: statement }, fieldType, flag);
         }
         options.typeChain?.push(new TypeChainEntry(resultType.name, resultType, options.flags, this.range));
         return resultType;
@@ -2330,6 +2364,7 @@ export class MethodStatement extends FunctionStatement {
 export class FieldStatement extends Statement implements TypedefProvider {
     constructor(
         readonly accessModifier?: Token,
+        readonly optional?: Token,
         readonly name?: Identifier,
         readonly as?: Token,
         readonly typeExpression?: TypeExpression,
@@ -2361,7 +2396,11 @@ export class FieldStatement extends Statement implements TypedefProvider {
     public readonly range: Range;
 
     public getLeadingTrivia(): Token[] {
-        return util.concatAnnotationLeadingTrivia(this, this.accessModifier?.leadingTrivia ?? this.name?.leadingTrivia ?? []);
+        return util.concatAnnotationLeadingTrivia(this, this.accessModifier?.leadingTrivia ?? this.optional?.leadingTrivia ?? this.name?.leadingTrivia ?? []);
+    }
+
+    public get isOptional() {
+        return !!this.optional;
     }
 
     transpile(state: BrsTranspileState): TranspileResult {
@@ -2386,8 +2425,12 @@ export class FieldStatement extends Statement implements TypedefProvider {
 
             result.push(
                 this.accessModifier?.text ?? 'public',
-                ' ',
-                this.name?.text,
+                ' '
+            );
+            if (this.isOptional) {
+                result.push(this.optional.text, ' ');
+            }
+            result.push(this.name?.text,
                 ' as ',
                 type.toTypeString()
             );
diff --git a/src/types/BscType.ts b/src/types/BscType.ts
index 842aee2a..c4e7b439 100644
--- a/src/types/BscType.ts
+++ b/src/types/BscType.ts
@@ -1,5 +1,5 @@
 import type { GetSymbolTypeOptions, SymbolTableProvider } from '../SymbolTable';
-import type { SymbolTypeFlag } from '../SymbolTable';
+import { SymbolTypeFlag } from '../SymbolTable';
 import { SymbolTable } from '../SymbolTable';
 import { BuiltInInterfaceAdder } from './BuiltInInterfaceAdder';
 import type { ExtraSymbolData, TypeCompatibilityData } from '../interfaces';
@@ -100,20 +100,24 @@ export abstract class BscType {
                 .getSymbolTypes(memberSymbol.name, { flags: flags })
                 ?.map(symbol => symbol.type);
             if (!targetTypesOfSymbol || targetTypesOfSymbol.length === 0) {
-                data.missingFields.push({ name: memberSymbol.name, expectedType: memberSymbol.type });
-                isSuperSet = false;
+                // eslint-disable-next-line no-bitwise
+                if (!(memberSymbol.flags & SymbolTypeFlag.optional)) {
+                    data.missingFields.push({ name: memberSymbol.name, expectedType: memberSymbol.type });
+                    isSuperSet = false;
+                }
+
             } else {
                 isSuperSet =
-                    (targetTypesOfSymbol ?? []).reduce((acc, typeOfTargetSymbol) => {
-                        if (!acc) {
-                            return acc;
+                    (targetTypesOfSymbol ?? []).reduce((superSetSoFar, typeOfTargetSymbol) => {
+                        if (!superSetSoFar) {
+                            return superSetSoFar;
                         }
 
                         const myMemberAllowsTargetType = memberSymbol.type.isTypeCompatible(typeOfTargetSymbol, { depth: data.depth });
                         if (!myMemberAllowsTargetType) {
                             data.fieldMismatches.push({ name: memberSymbol.name, expectedType: memberSymbol.type, actualType: targetType.getMemberType(memberSymbol.name, { flags: flags }) });
                         }
-                        return acc && myMemberAllowsTargetType;
+                        return superSetSoFar && myMemberAllowsTargetType;
                     }, true) && isSuperSet;
             }
 


Test patch:
diff --git a/src/parser/tests/statement/ConstStatement.spec.ts b/src/parser/tests/statement/ConstStatement.spec.ts
index 5e3704c5..531c9f0c 100644
--- a/src/parser/tests/statement/ConstStatement.spec.ts
+++ b/src/parser/tests/statement/ConstStatement.spec.ts
@@ -64,6 +64,21 @@ describe('ConstStatement', () => {
         `);
     });
 
+    it('allows const with the name `optional`', () => {
+        program.setFile('source/main.bs', `
+            const optional = true
+            namespace alpha
+                const optional = true
+            end namespace
+            sub main()
+                print optional
+                print alpha.optional
+            end sub
+        `);
+        program.validate();
+        expectZeroDiagnostics(program);
+    });
+
     describe('transpile', () => {
 
         it('transpiles simple consts', async () => {
@@ -247,7 +262,6 @@ describe('ConstStatement', () => {
                 end sub
             `);
             program.validate();
-
             expectCompletionsIncludes(
                 // log(|)
                 program.getCompletions('source/main.bs', util.createPosition(5, 34)),
diff --git a/src/parser/tests/statement/Enum.spec.ts b/src/parser/tests/statement/Enum.spec.ts
index 86822207..0aa101cc 100644
--- a/src/parser/tests/statement/Enum.spec.ts
+++ b/src/parser/tests/statement/Enum.spec.ts
@@ -202,6 +202,15 @@ describe('EnumStatement', () => {
     });
 
     describe('validation', () => {
+        it('allows enums named `optional`', () => {
+            program.setFile('source/main.bs', `
+                enum optional
+                    thing = 1
+                end enum
+            `);
+            program.validate();
+            expectZeroDiagnostics(program);
+        });
 
         it('catches duplicate enums from same file', () => {
             program.setFile('source/main.bs', `