Reputation: 18859
I have the following type and interfaces.
type ColVal = [{
col: string
}, {
val: string
}]
interface IEquals {
eq: ColVal,
}
interface INotEquals {
ne: ColVal,
}
And I have the following function:
const getOperation = (col: string, val: string, operation: string): IEquals | INotEquals => {
let op: 'eq' | 'ne' = operation === 'Equals' ? 'eq' : 'ne';
return {
[op]: [{
col,
}, {
val,
}]
};
};
But I get the error Type '{ [x: string]: ({ col: string } | { val: string; })[]; }' is not assignable to 'IEquals | INotEquals'.
If I change [op]
to ['eq']
or ['ne']
, the error goes away. Does anybody know how to fix this?
Here is the TypeScript playground for you guys to see the issue: Playground.
Upvotes: 1
Views: 329
Reputation: 329453
It's currently a design limitation of TypeScript that computed properties of union types have their key widened all the way to string
by the compiler. So an object literal like {[Math.random()<0.5 ? "a" : "b"]: 123}
is inferred by the compiler to be of the type {[k: string]:number}
instead of the more specific {a: number} | {b: number}
.
Both microsoft/TypeScript#13948 and microsoft/TypeScript#21030 concern this issue. It looks like there was some attempt to address it at one point, microsoft/TypeScript#21070) but it fizzled out.
I don't know if it will ever be addressed, but for now you'll have to work around it.
The least disruptive (and least type safe) workaround this is just to assert that the return value is of the appropriate type. In this case the compiler sees the return value is of a type that's so wide that it doesn't even see it as related to IEquals | INotEquals
. So you'll have to assert through some intermediate type... might as well just use any
, the ultimate "just make it work" type:
const getOperationAssert = (col: string, val: string, operation: string): IEquals | INotEquals => {
let op: 'eq' | 'ne' = operation === 'Equals' ? 'eq' : 'ne';
return {
[op]: [{
col,
}, {
val,
}]
} as any; // 🤓 I'm smarter than the compiler
};
Another idea is to manually implement a helper function that behaves the way computed properties "should" behave. Like this:
function computedProp<K extends PropertyKey, V>(key: K, val: V): { [P in K]: { [Q in P]: V } }[K];
function computedProp(key: PropertyKey, val: any) {
return { [key]: val };
}
So if you call computedProp(Math.random()<0.5 ? "a" : "b", 123)
, the implementation just makes an object with a computed property, but the typing is such that it returns {a: number} | {b: number}
. Then your getOperation()
becomes:
const getOperationHelper = (col: string, val: string, operation: string): IEquals | INotEquals => {
let op: 'eq' | 'ne' = operation === 'Equals' ? 'eq' : 'ne';
return computedProp(op, [{
col,
}, {
val,
}]);
};
Finally, if you're willing to refactor, you could consider not using computed properties at all. Store your ColVal
value in a variable, and then return it as the property of either an object literal with a literal eq
key or one with a literal ne
key. The compiler can follow that flow much more accurately and can verify it as safe:
const getOperationRefactor = (col: string, val: string, operation: string): IEquals | INotEquals => {
let op: 'eq' | 'ne' = operation === 'Equals' ? 'eq' : 'ne';
const colVal: ColVal = [{ col }, { val }];
return (operation === 'Equals') ? { eq: colVal } : { ne: colVal };
};
Hopefully one of those works for you.
Upvotes: 3