Nominal Type Support for Typescript



console.log("====================================");

/**
 * Type information
 * Type instance provide a isInstanceOf function for the interface type T
 */
interface Type<T> {
  id: string;
  assignableTypes: string;
  superTypes?: Type<any>[]
  isInstanceOf(obj: any): obj is T
}

/**
 * All Types needs to use Nominal Type System must implements this interface
 * Any instance of this type "T" can be assigned to types which id is in __assignableTypes, separated by '|'
 */
interface NominalTypeSupported{
  __assignableTypes: string
}

let TypeEnum = new Set<string>()

/**
 * 
 * @param typeId  must unique in app
 */
function createType<T extends NominalTypeSupported> (typeId: string, ...superTypes: Type<any>[]): Type<T>{
  if (TypeEnum.has(typeId)){
    throw Error("The type named '" + typeId + "' had been registered already. Check the conflict of the name.")
  }

  TypeEnum.add(typeId)

  let assignableTypes = calTypeId(typeId, ...superTypes)
  let typeObj = {
    id: typeId,
    superTypes: superTypes,
    assignableTypes: assignableTypes,

    /**
     * test if obj is a T instance.
     * @param obj 
     */
    isInstanceOf(obj: any): obj is T{  
      let objTypeId = (obj as T).__assignableTypes
      console.log("isInstanceOf: objTypeId="+objTypeId)

      // undefined false
      if (objTypeId === undefined) return false
      // single type
      console.log("isInstanceOf: __id="+ this.id)
      if (objTypeId === this.id) return true

      // union types
      let objTypeIds = objTypeId.split("|")
      for (const i in objTypeIds) {
        let id = objTypeIds[i]
        console.log("isInstanceOf: id="+ id)
        if (id === this.id) return true
      }

      return false
    }
  }

  console.log("type=" + typeId+"Type")
  console.log("assignableTypes=" + assignableTypes)
  console.log("typeId=" + typeId)
  return typeObj
}

/**
 * 
 * @param types 
 * @returns 
 */
function calTypeId(selfTypeId: string, ...types: Type<any>[]): string{
  let idSet = _calTypeId(selfTypeId, ...types)
  let idStr = ""
  idSet.forEach(id => {
    idStr += id + "|"
  })
  return idStr.substr(0, idStr.length - 1)

}

function _calTypeId(selfTypeId: string, ...types: Type<any>[]): Set<string>{
  let resultTypeIds = new Set<string>();
  if (selfTypeId.length > 0){
    resultTypeIds.add(selfTypeId)
  }

  for (const type of types) {
    resultTypeIds.add(type.id)
    if (type.superTypes && type.superTypes.length > 0){
      let superIds = _calTypeId("", ...type.superTypes)
      for (const id of superIds) {
        resultTypeIds.add(id)
      }
    }
  }
  return resultTypeIds
}


// export default {createType, Type<T>, NominalTypeSupported}


//// test codes/////////

interface AbilityA extends NominalTypeSupported{
  a: string
}
let AbilityAType = createType<AbilityA>("AbilityA")

interface AbilityB extends NominalTypeSupported{
  b: string
}
let AbilityBType = createType<AbilityB>("AbilityB")

interface AbilityC extends AbilityA  {
}
let AbilityCType = createType<AbilityC>("AbilityC", AbilityAType)

///////////////////////
class ListItem{}

class ListItemA extends ListItem implements AbilityA, AbilityB{
  __assignableTypes: string = ListItemAType.assignableTypes
  
  a: string = "";
  b: string = "";
}
// 类型对象的规则: T=类型, self_id=T的id, superTypes: extends 或 implments的 (NominalTypeSupported) T
let ListItemAType = createType<ListItemA>("ListItemA", AbilityAType, AbilityBType)


class ListItemB extends ListItem implements AbilityC{
  __assignableTypes: string = ListItemBType.assignableTypes;  
  a: string = "";
}
let ListItemBType = createType<ListItemB>("ListItemB", AbilityCType)

class ListItemC extends ListItemA {
  __assignableTypes: string = ListItemCType.assignableTypes
}
let ListItemCType = createType<ListItemC>("ListItemC", ListItemAType)

class ListItemD extends ListItemC implements AbilityC {
  __assignableTypes: string =  ListItemDType.assignableTypes
}
let ListItemDType = createType<ListItemD>("ListItemD", ListItemCType, AbilityCType)


let item: ListItem = new ListItemA
console.log("typeId =" + (item as any).__assignableTypes)
console.log(AbilityAType.isInstanceOf(item)) // expect true
console.log(AbilityBType.isInstanceOf(item)) // expect true
console.log(AbilityCType.isInstanceOf(item))  // expect false

let item2: ListItem = new ListItemB
console.log(AbilityCType.isInstanceOf(item2))  // expect true
console.log(AbilityAType.isInstanceOf(item2))  // expect true
console.log(AbilityBType.isInstanceOf(item2))  // expect false

// throw Error
try{
  createType<ListItemD>("ListItemD", ListItemCType, AbilityCType)
}catch (e){
  console.log(e)
}