简体   繁体   中英

How to narrow down the union type of a class generic?

This class holds either a single Item or an array of items Item[] , determined by a setting at runtime ( this.config.isMultiple ).

How can I narrow down the class' generic type with a type guard to Item[] in the whole if (this.isMultiple) -Block? (which has many, many more accesses than this example, each needing to have an individual typecast, otherwise).

Minimum working example:

interface Item { /* ... */ }

class Demo <DATATYPE extends Item | Item[]> {
  config: { isMultiple: boolean; }
  value: DATATYPE;

  get isMultiple(): boolean {
    return this.config.isMultiple;
  }

  addValue(value: Item) {
    if (this.isMultiple) {
      if (!Array.isArray(this.value)) {
        this.value = [] as Item[];
        // TS2322: Type 'Item[]' is not assignable to type 'DATATYPE'. 'Item[]' is assignable to the constraint of type 'DATATYPE', but 'DATATYPE' could be instantiated with a different subtype of constraint 'Item | Item[]'.
      }
      this.value.push(value);
      // TS2339: Property 'push' does not exist on type 'Item | Item[]'. Property 'push' does not exist on type 'Item'.
    } else {
      this.value = value;
      // TS2322: Type 'Item' is not assignable to type 'DATATYPE'. 'Item' is assignable to the constraint of type 'DATATYPE', but 'DATATYPE' could be instantiated with a different subtype of constraint 'Item | Item[]'.
    }
  }
}

Union This

Provide this type to the addValue function in the class.

interface MultipleDemoThis<T> {
     config: { isMultiple: true; }
     value: T[];
     get isMultiple(): true;
}

interface SingleDemoThis<T> {
    config: { isMultiple: false; }
    value: T;
    get isMultiple(): false;
}

type DemoThis<T> = MultipleDemoThis<T> | SingleDemoThis<T>

interface Item { /* ... */ }

class Demo<T extends Item> {
  config = { isMultiple: true }
  get isMultiple(): boolean {
    return this.config.isMultiple;
  }

  addValue(this: DemoThis<T>, value: T) {
    if (this.isMultiple) {
      if (!Array.isArray(this.value)) {
        this.value = [];
      }
      this.value.push(value);
    } else {
      this.value = value;
    }
  }
}

TypeScript Playground

Narrowing using multi-variables

class Demo <DATATYPE extends Item> {
  config: { isMultiple: boolean }
  value?: DATATYPE
  values?: DATATYPE[]
  
  get isMultiple(): boolean {
    return this.config.isMultiple
  }

  // Please use a getter or a function to access the data.
  //   this is an example
  getValue() {
    if(this.isMultiple) {
      return this.values
    }
    return this.value
  }

  addValue(value: DATATYPE) {
    if (this.isMultiple) {
      if (!Array.isArray(this.values)) {
        this.values = []
      }
      this.values.push(value)
    } else {
      this.value = value
    }
  }
}

You may need to clean this.values after switching isMultiple .

Narrowing / Forcing using as keyword

class Demo <DATATYPE extends Item | Item[]> {
  config: { isMultiple: boolean; }
  value: DATATYPE;

  get isMultiple(): boolean {
    return this.config.isMultiple;
  }

  addValue(value: Item) {
    if (this.isMultiple) {
      if (!Array.isArray(this.value)) {
        this.value = <Item>[] as DATATYPE
      }
      // we can be sure it is an array, so we use `as` to tell TS it must be an Array.
      (this.value as Item[]).push(value)
    } else {
      this.value = value as DATATYPE
    }
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM