我有一个 TypedMapper 实用程序类,它接受:
T
T[]
从流程的角度来看,它看起来像这样:
为了使这些双重结构成为输入/输出,我将以下作为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()函数返回离散类型T
或T[]
而不是这两种类型的并集。例如,在以下单元测试中,我们获得了成功的JS结果,但的类型不为T
或T[]
:
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);
如以下屏幕截图所示:
任何人都可以帮助我理解如何确保 - 基于输入结构是否为数组 - 输出数据结构是离散已知的?
答案 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
<小时/> 我不知道这是否算作'输出结构是离散的'。