0

sorry for the confusing title,i'm not sure how to explain my question clearly. currently, i'm try to implement a method which could parse a object from a xml. the xml (use xml-js to translate to js) looks like this

interface OpmlElement {
  attributes: {
    text:string
  },
  elements:OpmlElement[]
}

the target object look like this

interface ParsedTestCase {
    title?: string;
    suites?: string[];
}

so, i want to declare some kind of parser to parse this xml. idefine some kind of parser

const elementParserTable= [
  {
        check: (e:OpmlElement) => getText(e).startsWith("tt:"),
        takeValue: (e:OpmlElement) => getText(e),
        cb: (v:string)=> {
          testcase.title=v
        }
  } ,
  {
        check: (e:OpmlElement) => getText(e).startsWith("ts:"),
        takeValue: (e:OpmlElement) =>getText(e).split(","),
        cb: (v:string[])=> {
          testcase.suites=v
        }
  },
]

the first question is when i use like above

const elements:OpmlElement[]=[]

for (const e of elements) {
    for (const elementParser of elementParserTable) {
        if (elementParser.check(e)) {
            elementParser.cb(elementParser.takeValue(e));
            continue;
        }
    }
}

ts complains that
enter image description here

Argument of type 'string | string[]' is not assignable to parameter of type 'string & string[]'. Type 'string' is not assignable to type 'string & string[]'. Type 'string' is not assignable to type 'string[]'.

how could i avoid this? and the second question is: is there some way to constrain the elementParser in elementParserTable to make sure that the return type of takeValue function is the cb function's parameter type?

Vna
  • 532
  • 6
  • 19
Cong Wu
  • 372
  • 2
  • 16

3 Answers3

1

I'm not sure that you can avoid your error without casting ( elementParser.cb(elementParser.takeValue(e) as any ).

To answer the second question, you can make sure that the return type of take value is cb's parameter with a generic interface. For example:

interface ElementParser<T> {
    check: (e: OpmlElement) => boolean,
    takeValue: (e: OpmlElement) => T,
    cb: (v: T) => void
}

const elementParserTable: Array<ElementParser<string> | ElementParser<string[]>> = [...]

Of course, all of this is just a crappy workaround of the fact that typescript does not have existential types.

Rubydesic
  • 3,386
  • 12
  • 27
0

I would recommend combine takeValue and cb at one method.

const elementParserTable= [
  {
    check: (e: OpmlElement) => getText(e).startsWith("tt:"),
    cb: (e: OpmlElement) => {
      testcase.title = getText(e);
    },
  },
  {
    check: (e: OpmlElement) => getText(e).startsWith("ts:"),
    cb: (e: OpmlElement)=> {
      testcase.suites = getText(e).split(",");
    },
  },
];

And usage:

const elements: OpmlElement[] = [];

for (const e of elements) {
  for (const elementParser of elementParserTable) {
    if (elementParser.check(e)) {
      elementParser.cb(e);

      continue;
    }
  }
}

I think you can try combine check, takeValue and cb at one method. But I'm not sure if that's your case.

Kirill Skomarovskiy
  • 1,535
  • 1
  • 7
  • 9
0

You can do another function to let typescript know if it's a string

    isTitle(titleOrSuite:string|string[]) titleOrSuite is string{
       return typeof titleOrSuite === "string";
    }
const elementParserTable= [
  {
        check: (e:OpmlElement) => getText(e).startsWith("tt:"),
        takeValue: (e:OpmlElement) => getText(e),
        cb: (v:string|string[])=> isTitle(v) ? testcase.title=v : testcase.suites=v      
  } ,
  {
        check: (e:OpmlElement) => getText(e).startsWith("ts:"),
        takeValue: (e:OpmlElement) =>getText(e).split(","),
        cb: (v:string|string[])=> isTitle(v) ? testcase.title=v : testcase.suites=v
  },
]
Twen
  • 302
  • 2
  • 6