接受T或Array <t>即使使用Union类型也不起作用

时间:2017-08-03 23:18:27

标签: typescript

我有一个 TypedMapper 实用程序类,它接受:

  • 类型为T
  • 的哈希/字典输入
  • 类型为T[]
  • 的哈希/词典数组

从流程的角度来看,它看起来像这样:

flow diagram

为了使这些双重结构成为输入/输出,我将以下作为map()函数:

public map() {

  return Array.isArray(this._inputData)
    ? this._inputData.map((item: T) => this.convert(item)) as T[]
    : this.convert(this._inputData) as T;
}

TypedMapper类工作正常,但是当我使用它时,我希望map()函数返回离散类型TT[]而不是这两种类型的并集。例如,在以下单元测试中,我们获得了成功的JS结果,但的类型不为TT[]

const config = {
  passThroughs: ['foo', 'bar', 'baz'],
  defaults: {
    foo: 12345
  }
};
const data = {
  bar: 'hello world'
};
interface IFooBarBaz {
  foo: number;
  bar: string; 
  baz: string;
}
const mapped = new TypedMapper<IFooBarBaz>(data, config).map();
expect(mapped.foo).to.equal(12345);
expect(mapped.bar).to.equal('hello world');
expect(Object.keys(mapped)).to.include('baz');
expect(mapped.baz).to.equal(undefined);

如以下屏幕截图所示:

ts error

任何人都可以帮助我理解如何确保 - 基于输入结构是否为数组 - 输出数据结构是离散已知的?

3 个答案:

答案 0 :(得分:3)

这是一个非常有趣的问题。如果TypeScript执行type inference on optional generic types会很棒;那么我认为我们可以将TypedMapper<T>扩展为TypedMapper<T,D extends T | T[]>并允许从构造函数中推断D。然后map()的输出将为D,您就完成了所有工作。

但它没有。

可以做的是这样的:创建TypedMapper<T>的两个子类(或子接口),如下所示:

interface ArrayTypedMapper<T> extends TypedMapper<T> {
  map(): T[]
}
interface NonArrayTypedMapper<T> extends TypedMapper<T> {
  map(): T
}

并创建一个静态方法,而不是构造函数:

class TypedMapper<T> {
...
  static make<T>(inputData: Partial<T>[], config: any) : ArrayTypedMapper<T>
  static make<T>(inputData: Partial<T>, config: any): NonArrayTypedMapper<T>
  static make<T>(inputData: Partial<T>[] | Partial<T>, config: any): TypedMapper<T> {
    return new TypedMapper<T>(inputData, config); 
  }

现在,重载的make函数会识别inputData是否为数组,并返回缩小的TypeMapper<T>类型:

const mapped = TypedMapper.make<IFooBarBaz>(data, config).map();
// mapped: IFooBarBaz
const mappedArr = TypedMapper.make<IFooBarBaz>([data], config).map();
// mappedArr: IFooBarBaz[]

这是我能得到的尽可能接近。可能还有其他方法可以做到,但我实际可以实现的所有方法都涉及TypedMapper<T>的专用子类。

希望有所帮助。祝你好运!

答案 1 :(得分:1)

据我所知,类型推断并不跨越函数边界。

您可以使用overloads表达您想要的内容,但是map()必须以data作为参数,然后您可以为map声明两次重载,对象和返回对象,另一个采用数组并返回数组。

很难给出确切的答案,因为你没有在你的问题中给出TypedMapper的定义,但是这样的事情可能有用:

class TypedMapper<T, D> {
    constructor(public config: { passThroughs: string[], defaults: Partial<T> }) {
    }
    public map(d: D): T;
    public map(d: D[]): T[];
    public map(d: D | D[]): T| T[] {
         return Array.isArray(d)
                ? d.map((item: D) => this.convert(item)) as T[]
                : this.convert(d);
    }
    public convert(data: D): T {
        return undefined;
    }
}

const config = {
  passThroughs: ['foo', 'bar', 'baz'],
  defaults: { foo: 12345 }
};
const data = {
  bar: 'hello world'
};

interface IFooBarBaz {
  foo: number;
  bar: string; 
  baz: string;
}

const mapped = new TypedMapper<IFooBarBaz, typeof data>(config).map(data);
mapped.foo === 12345;
mapped.bar === 'hello world';

const a = [data, data];

const mappedArray = new TypedMapper<IFooBarBaz, typeof data>(config).map(a);
mappedArray[0].foo === 12345;

答案 2 :(得分:0)

我会给你一些(可能是不满意的)选择:

第一种选择是显式地转换该映射值:

expect((<IFooBarBaz>mapped).bar).to.equal('hello world');

更有趣的选择是将其包裹在一个类型后卫中。

   if(!Array.isArray(mapped)) {
        expect(mapped.bar).to.equal('hello world');  // No explicit casting!
        expect(mapped.baz).to.equal(undefined);
   } else {
        // Fail the test here!
   }

在这种情况下,typescript编译器足够聪明地应用逻辑:因为if块中的代码仅在mapped不是数组时运行,mapped必须是类型IFooBarBaz

<小时/> 我不知道这是否算作'输出结构是离散的'。