I have a big object literal and the type inference is perfect except one property, which I have to cast:
const obj = {
a: 'big',
ol: 'object',
with: 'a lot of properties',
nums: [] as number[], // would be undefined[] or unknown[] without cast
}
This works perfectly well except the as irks me and shows up in my linter and my running total-coercion-counter. I can also do const nums: number[] = [] on a separate line but then I'm using multiple declarations for what's really one object. Writing out the whole interface is unnecessary and verbose. Is there any kind of inline soft-cast or hint so I can get the : number[] benefits with the inline-ness of as? Angle bracket casts also make my linter angry plus I'm using JSX.
CodePudding user response:
You are looking for something like a "widening-only assertion" or a "satisfies operator" as requested in microsoft/TypeScript#7481. The idea is that you'd write something like
const obj = {
a: 'big',
ol: 'object',
with: 'a lot of properties',
nums: [] satisfies number[] // <-- not valid as of TS4.5
}
and the compiler would contextually interpret [] to be a type assignable to number[], and give an error if it can't. That would be safer than a type assertion (which you're calling "casting" or "coercion") which will allow you to unsafely narrow a value to a type. Currently "hello" as "goodbye" is allowed, but "hello" satisfies "goodbye" would be an error.
Anyway, there is currently no built-in way to do this in TypeScript. There was a recent design discussion in microsoft/TypeScript#46872 and microsoft/TypeScript#46878, and there's a possible implementation at microsoft/TypeScript#46827. But it's not clear when this will make it into the language. For now there are only workarounds.
A common workaround is to have a generic helper identity function:
const satisfies = <T,>(x: T) => x;
And then write satisfies<Type>(value) in place of value satisfies Type:
const obj = {
a: 'big',
ol: 'object',
with: 'a lot of properties',
nums: satisfies<number[]>([]) // <-- this works
}
/* const obj: {
a: string;
ol: string;
with: string;
nums: number[];
} */
Now the compiler sees nums as type number[], as desired. This version of satisfies also gives compiler errors in cases where a type assertion succeeds:
"hello" as "goodbye"; // no error
satisfies<"goodbye">("hello") // error!
So this workaround is about as good as it gets in current TypeScript. Yes, it has observable runtime effects since the emitted JavaScript now has an extra function call, but it's a fairly innocuous change. If you're only going to do such an operation once in your code base, then you might as well write const nums: number[] = []. But if you run into this issue at least twice, then the satisfies helper function probably pays for itself.
