FSL-defined function type errors

Hey there, I have a relatively simple function defined in FSL, which is causing a few type errors that I’m having trouble fixing.

I’d like to provide either an Asset, Incident or null to the function, and have it return an array of documents of different possible types.

function fetchRelatedDocs(doc: Ref<Asset> | Ref<Incident> | null): Array<DocumentDef> {
    if (doc == null) {
        []
    }
    else {
        let coll = doc!.coll
        let id = doc!.id
        
        []
    }
}

There are several errors this short block throws:

cause: Type `null` does not have field `coll`
 ›   at functions/lib/fetchRelatedDocs.fsl:100:25
 ›       |
 ›   100 |         let coll = doc!.coll
 ›       |                         ^^^^
 ›       |
 ›   hint: Type `null` inferred here
 ›      |
 ›   95 | function fetchRelatedDocs(doc: Ref<Asset> | Ref<Incident> | null): Array<DocumentDef> {
 ›      |                                                                                                                                      ^^^^
 ›      |
 ›   cause: Type `null` does not have field `id`
 ›   at functions/lib/fetchRelatedDocs.fsl:101:23
 ›       |
 ›   101 |         let id = doc!.id
 ›       |                       ^^
 ›       |
 ›   hint: Type `null` inferred here
 ›      |
 ›   95 | function fetchRelatedDocs(doc: Ref<Asset> | Ref<Incident> | null): Array<DocumentDef> {
 ›      |                                                                                                                                      ^^^^
 ›      |
 ›   cause: Type `{ owner: Ref<User>, name: String, id: ID, ts: Time, ttl: Time | Null }` is not a subtype of `DocumentDef`
 ›       |
 ›    95 |   function fetchRelatedDocs(doc: Ref<Asset> | Ref<Incident> | null): Array<DocumentDef> {
 ›       |  _^
 ›    96 | |     if (doc == null) {
 ›    97 | |         []
 ›    98 | |     }
 ›    99 | |     else {
 ›   100 | |         let coll = doc!.coll
 ›   101 | |         let id = doc!.id
 ›   102 | |
 ›   103 | |         []
 ›   104 | |     }
 ›   105 | | }
 ›       | |_^
 ›       |
 ›   cause: Type `Null` is not a subtype of `DocumentDef`
 ›       |
 ›    95 |   function fetchRelatedDocs(doc: Ref<Asset> | Ref<Incident> | null): Array<DocumentDef> {
 ›       |  _^
 ›    96 | |     if (doc == null) {
 ›    97 | |         []
 ›    98 | |     }
 ›    99 | |     else {
 ›   100 | |         let coll = doc!.coll
 ›   101 | |         let id = doc!.id
 ›   102 | |
 ›   103 | |         []
 ›   104 | |     }
 ›   105 | | }
 ›       | |_^
 ›       |

A few things about this I’m not too sure I understand:

  • I couldn’t find documentation for a return value that is just a Doc of any type… I’d seen CollectionDef before, so I guessed DocumentDef might exist. Does it?
  • Is there a difference between Ref<Asset> and Asset in FSL?

Interested in any guidance you can provide to help. Thanks in advance!

Hi @danc and welcome! :wave:

nulls and references

The null handling here is not working correctly. From what I can tell you are using ! correctly to handle possibly missing documents.

This might be related to how we compare Refs to null and the fact that you also type | null. Current behavior for ref == null is to try to read the document and return true if the document doesn’t exist. In your case, the variable might be literally a null value, so you may need one assertion that the variable itself is not null, then another assertion that the reference is not dangling.

NOTE: we want to make it easier and clearer how to handle the difference between a literal null value and a dangling reference, so we may change the behavior of ref == null in the future – along with any versioning, documentation, and communication necessary for such a change.

Document types

DocumentDef is not a type. CollectionDef is the system document that records the definition of your collections. The types for Documents are based on the name of your documents.

The type for any random document is { id: ID, ts: Time, ttl: Time | Null, *: Any }, as shown in this error message.

You can use that, or you can specify a Union of types.

Yes. A Ref represents a pointer to a potentially non-existent document. Ref<Product> is really just shorthand for Product | NullProduct (See NullDoc type info), whereas the type Product means a reference that is known to exist.

Updated UDF

Does something like either of these fix things? One thing I’ve done in both is to shadow the doc variable with a new type.

require a valid reference as an argument

function fetchRelatedDocs(doc: Asset | Incident | null): Array<{ id: ID, ts: Time, ttl: Time | Null, *: Any }> {
    if (doc == null) {
        []
    } else {
        let doc: Asset | Incident = doc!
        // get related docs...
    }
}

Handle dangling references

function fetchRelatedDocs(ref: Ref<Asset> | Ref<Incident> | null): Array<{ id: ID, ts: Time, ttl: Time | Null, *: Any }> {
    if (ref == null) {
        []
    } else {
        let ref: Ref<Asset> | Ref<Incident> = ref!
        if (ref.exists()) {
            let doc: Asset | Incident = ref!
            // get related docs...
        } else {
            []
        }
    }
}

Hey @ptpaterson! I’ve been reading your replies on this forum for years, so this interaction feels akin to meeting a celebrity. Thanks for taking the time to help.

Everything you’ve said makes sense, but I can’t seem to make the concepts work in the FSL.

Simplified Example

For simplicity, I’ve reduced the code down to this:

collection TypeA {
    typeAField: String
}

collection TypeB {
    typeBField: String
}

function test(ref: Ref<TypeA> | Ref<TypeB> | null): Array<TypeA | TypeB> {
    if (ref == null) {
        []
    }
    else {
        let realRef: Ref<TypeA> | Ref<TypeB> = ref!
        if (realRef!.exists()) {
            // logic for returning more docs, but for now mocked with empty array
            []
        }
        else {
            []
        }
        
    }
}

Two collections (TypeA, TypeB) are defined, and the test() function expects one of Ref<TypeA>, Ref<TypeB>, or null.

Why null is a possible argument

As an aside, my reasoning for wanting this function to also accept a direct null value is so that it can be called directly with optional ref fields. An example usage might be:

let refOrNull = typeADoc?.optionalTypeBRef
let testResult = test(refOrNull)

Error Log

The above FSL code still results in the following error, which appears to have a lot of listed causes:

›   Error: error: Type `Ref<TypeA> & Any & Ref<TypeB> => []` is not a subtype of `(ref: null | Ref<TypeA | TypeB>) => Array<TypeA | TypeB>`
 ›   at functions/lib/test.fsl:103:1

I’m confused by the first part of this error, specifically the Ref<TypeA> & Any & Ref<TypeB> => []… I understand that the => [] may be inferred due to returning the empty array ([]) at the end of every conditional, but how/why is the first part (Ref<TypeA> & Any & Ref<TypeB>) inferred?

Here are the listed causes beneath the error. It appears that all of the argument types are being compared to each other?

Particularly confusing is the null is not a subtype of Null… are null and Null different??

 ›   cause: Type `{ typeAField: String, id: ID, ts: Time, ttl: Time | Null }` contains extra field `typeAField`
 ›   cause: Type `{ typeAField: String, id: ID, ts: Time, ttl: Time | Null }` does not have field `typeBField`
 ›   cause: Type `{ typeAField: String, id: ID, ts: Time, ttl: Time | Null }` is not a subtype of `Null`
 ›   cause: Type `null` is not a subtype of `{ typeAField: String, id: ID, ts: Time, ttl: Time | Null }`
 ›       |
 ›   103 | function test(ref: Ref<TypeA> | Ref<TypeB> | null): Array<TypeA | TypeB> {
 ›       |                                              ^^^^
 ›   cause: Type `null` is not a subtype of `Null`
 ›       |
 ›   103 | function test(ref: Ref<TypeA> | Ref<TypeB> | null): Array<TypeA | TypeB> {
 ›       |                                              ^^^^
 ›   cause: Type `null` is not a subtype of `{ typeBField: String, id: ID, ts: Time, ttl: Time | Null }`
 ›       |
 ›   103 | function test(ref: Ref<TypeA> | Ref<TypeB> | null): Array<TypeA | TypeB> {
 ›       |                                              ^^^^

Thanks again in advance for your time and help.

1 Like

Ok, so I may have discovered the difference between null and Null (but please correct me if I’m wrong). I think Null is a type while null is the actual value of null?

I updated the previous function declaration to use Null instead of null for the typing, and it reduced the errors thrown.

New function definition:

function test(ref: Null | Ref<TypeA> | Ref<TypeB>): Array<TypeA | TypeB> {}

That got rid of the two last errors in the previous post, but the first three are still there. The remaining errors include:

 ›   cause: Type `{ typeAField: String, id: ID, ts: Time, ttl: Time | Null }` contains extra field `typeAField`
 ›   cause: Type `{ typeAField: String, id: ID, ts: Time, ttl: Time | Null }` does not have field `typeBField`
 ›   cause: Type `{ typeAField: String, id: ID, ts: Time, ttl: Time | Null }` is not a subtype of `Null`

The type-checker seems to still be comparing the three argument types and throwing an error if they aren’t subtypes of one another… unless I’m reading the error messages incorrectly.

Excited to hear your thoughts. Getting closer!

Ok, so I think it turns out the issue is not in the function declaration, but in this line:

let realRef: Ref<TypeA> | Ref<TypeB> = ref!

I’d recalled an exchange via the Fauna discord where someone recommended casting a doc to Any in order to resolve some type errors, so I tried that for this line like so:

let realRef: Any = ref!

And for some reason that worked. It did raise another related question, but I’ll open a new topic to keep things tidy and link that here once I do.

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.