How in Typescript transform interface members to Tagged Template Literal (TTL)?

306 Views Asked by At

My primary idea is to use Typescript ecosystem as an "editor" to create markdown SPECifications based on "domain" examples - the type safest possible way, with autocomplete and "notifications" for compilation errors

So far I have this code with stated PROBLEMs:

/* TYPES.ts */

// a SPEC-ification TTL (implementation is omited, it is only prototype for now)
declare function SPEC (docs: TemplateStringsArray, ...domainExamples: Glossary[]): ExFnc
type ExFnc = (optionalOverloadingDomainExample?: Object) => string

// ... rest of types are omited here - pls see them by link to TS Playground


/* DOMAIN_GLOSSARY.ts */

// At   = Attribute
// roAt = read-only Attribute
// Ac   = Action
type Glossary
//      Label              Example 1                       Example N
= At    <`Url`,           `system.org/login`>
| At    <`User Name`,     `[email protected]`             | `[email protected]`>
| At    <`Password`,      `existing`                     | `invalid`>
| Ac    <`Login`>
| roAt  <`Error message`,                                  `Invalid User Name or Password`>


/* SPECIFICATION_BY_EXAMPLE.ts */

const S1_Alt2 = SPEC
`
## Alternative scenario: Invalid login

- Given
  - ${`Url: system.org/login`}

- When
  - ${`User Name: [email protected]`}   
  - ${`Password: invalid`}
  - ${`Login`}

- Then
  - ${`Error message? Invalid User Name or Password`}
`

/* TEST_CASES.ts */

// Case here is OK Scenario call - because there STILL is "original" domain Example
S1_Alt2 () 

// Case here is OK Scenario call - because "correctly" overloading original domain Example
S1_Alt2 ({ 

  'User Name':  `[email protected]`,
  'Password':   `existing`
})

// Case here is KO Scenario call - because "INcorrectly" overloading original domain Example
// and I expected stated "approximate" compile errors
S1_Alt2 ({ 
           

  'Useeer Name':  `[email protected]`, 
   // Object literal may only specify known properties, and ''Useeeer Name'' does not exist in type '...'

  'Password':     `exiiisting`,            
   // Type '"exiiisting"' is not assignable to type '"existing"'.
   // The expected type comes from property 'Password' which is declared here on type '...'

   'Home page title': 
   // Object literal may only specify known properties, and ''Home page title'' does not exist in type '...' 
   // (pls note: this is because `Home page title...` was NOT interpollated in const S1_Alt1 = SPEC<uiLogin>`...` )
})

TS playground with complete code and types so far

1. PROBLEM - type ExFnc:

  • how to declare its single param type Object - so that Object properties:
    • can only be from Glossary Attributes but not Actions
    • can only be from interpollated ones
    • have to be optional
    • and all that any deep level
  • I think this PROBLEM alone - is big CHALLANGE!!!

2. PROBLEM - concise "Glossarifying"

  • I am looking for more concise syntax to "Glossarify" - based on interfaces
  • resp. how transform interfaces to "type safe" tagged template literals ?
    • all function members of interface should be transformed as "actions" in TTL
    • and all NONfunction members as "attributes"

3. PROBLEM - type safe call of S1_Alt2 ({ with autocomplete })

  • pls see object literal in the last call of S1_Alt2 - 'Useeer Name' property or 'exiiisting' value - here I expect compile errors
  • or if Login stated here - I expect compile error too
    • because Actions can't be "overloaded" (only Attributes can) - else we have "ANOTHER/STRANGE" "Scenario" (imagine Cancel Action in Scenario call !!!)
  • or if Home page title... stated here - I expect compile error too
    • because Attribute Home page title... was NOT interpollated in original const S1_Alt2 = SPEC`...`
  • and all Attributes should be optional but with autocomplete here
    • because they are already stated in const S1_Alt2

Pls any suggestions ?

1

There are 1 best solutions below

5
qwertys On

FOR THIS ANSWER - ALL CREDITS HERE GOES TO @captain-yossarian

(note: there is still big CHALLENGE with 1. PROBLEM - but rest is already on the right track :)

H I N T: for big picture pls start reading from /* I. (domain) GLOSSARY.ts */ part

/* 0. TYPES.ts */

// SPEC-ification is Tagged Template Literal based on <UserInterface>, interpolated with Domain Glossary terms (aka Domain Attributes & Actions)  
// (TTL implementation is not important here)
declare function SPEC<UI> (docs: TemplateStringsArray, ...domainExamples: Glossarify<UI>[]): 
  (optionalOverloadingDomainExample?: Partial<UI> ) => string

// GLossarify is type to transform an "User" Interface to Domain Glossary Attributes & Actions
type Glossarify<Obj> = {
  [Key in keyof Obj]:
    
  Obj[Key] extends Action ? ActionSyntax<Key> 
  // lets define interface FUNCTION member AS an ACTION in Domain resp. button/link element in UI - like:
  // `Login`                         --> here with CLICK operator allowed only
  
  : Obj[Key] extends Attribute ? AttributePlaceholderSyntax<Key> | AttributeSetSyntax<Obj, Key> | AttributeAssertSyntax<Obj, Key>
  // lets define REST interface members AS ATTRIBUTES in Domain resp. input elements in UI - like:
  // `User Name: [email protected]`  --> here with SET operator syntax on Attribute / UI element
  // `User Name? [email protected]`   --> here with ASSERT operator syntax against Attribute / UI element (to assert default "onLoad" value for example)
  // `User Name`                     --> here with PLACEholder only syntax :)
  
  : ActionSyntax<Key> | Glossarify<Obj[Key]>
  // lets define DEEP KEY also AS ACTION | deep recursion

}[keyof Obj]

// Attributes & Actions helpers
type Attribute                                          = string | boolean | number | bigint | symbol | null | undefined
type AttributePlaceholderSyntax<Key>                    = `${Key & string}${placeholder}`
type AttributeSetSyntax<Obj, Key extends keyof Obj>     = `${Key & string}${set} ${Obj[Key] & string}`
type AttributeAssertSyntax<Obj, Key extends keyof Obj>  = `${Key & string}${assert} ${Obj[Key] & string}`

type Action                                             = Function
type ActionSyntax<Key>                                  = `${Key & string}${click}`

// "operators" for Attributes & Actions
type placeholder  = ``
type set          = `:`
type assert       = `?`
type click        = `` // `` here is confusing, I know - and yes, you can imagine something like `*` instead 
                       // but `Login` syntax is shorter for me - than `Login*` ;)


                       
/* I. (domain) GLOSSARY.ts */

interface uiLogin {
// Domain Attribute/Action  Domain Example 1    | Domain Example N
        'Url':             `system.org/login`
        'User Name':       `[email protected]`   | `[email protected]`                   
        'Password':        `existing`           | `invalid`                  
        'Login': Action

        'Error message':                          `Invalid User Name or Password`
        
        'Home page title': `Welcome The.Valid !`
 
        Deep: {
          Att1:            `value1`             | `value2`
          Action1: Action
        }

// and yes - all should idealy work at any interface "deep" level :)        
}

interface uiPage_N { 
  // ...
}



/* II. (living) SPECIFICATION_BY_EXAMPLES.ts */

const S1_Happy = SPEC<uiLogin>
`
## Happy scenario: Succesful login

- Given
  - ${`Url: system.org/login`}

- When
  - ${`User Name: [email protected]`}
  - ${`Password: existing`}
  - ${`Login`}

- Then
  - ${`Home page title? Welcome The.Valid !`}
`

const S1_Alt1 = SPEC<uiLogin>
`
## Alternative scenario: Invalid login

- Given
  - ${`Url: system.org/login`}

- When
  - ${`User Name: [email protected]`}   
  - ${`Password: invalid`}
  - ${`Login`}

- Then
  - ${`Error message? Invalid User Name or Password`}
`


/* III. (test) CASES.ts */

// Case here is OK Scenario spec call - because with "original" domain Examples
S1_Happy()

// Case here is OK Scenario spec call - because correctly "overloading" original domain Examples
S1_Alt1({

  'User Name': `[email protected]`,
  'Password': `existing`
})

// Case here is KO Scenario spec call - because INcorrectly overloading original domain Examples
// and I expected stated "approximate" compile errors
S1_Alt1 ({ 

  'Useeer Name':  `[email protected]`, 
   // Object literal may only specify known properties, and ''Useeeer Name'' does not exist in type '...'

  'Password':     `exiiisting`,            
   // Type '"exiiisting"' is not assignable to type '"existing"' | ...
   // The expected type comes from property 'Password' which is declared here on type '...'

  'Home page title': 
  // Object literal may only specify known properties, and ''Home page title'' does not exist in type '...' 
  // (pls note: this is because `Home page title...` was NOT interpollated in "original" const S1_Alt1 = SPEC<uiLogin>`...` )
})