Coaxing the compiler into inferring literal types for values is tricky when those values are contained as array elements or object properties. Here is one possible way to go about it:
type Narrowable = string | number | boolean | object | {} | null | undefined | void;
const tupleOfNarrowObjects =
<V extends Narrowable, O extends { [k: string]: V }, T extends O[]>(...t: T) => t;
const my_array = tupleOfNarrowObjects(
{
foo: "hello",
bar: "Typescript"
}, {
foo: "goodbye",
bar: "JavaScript"
}
);
If you do that, my_array will now be inferred as the following type:
const my_array: [{
foo: "hello";
bar: "Typescript";
}, {
foo: "goodbye";
bar: "JavaScript";
}]
which is as narrow as it gets: a tuple of objects whose values are string literals. So the compiler knows that my_array[0].foo is "hello". And if you iterate over my_array the compiler will treat each element as a discriminated union.
How it works:
The Narrowable type is essentially the same as unknown, except it is seen by the compiler as a union containing string and number. If you have a generic type parameter V extends N where the constraint N is string or number or a union containing them, then V will be inferred as a literal type if it can be.
Usually, when a type parameter O is inferred to be an object type, it does not narrow the property types to literals. However, when O is constrained to an index-signature type whose value type is narrowable to a literal, like { [k: string]: V }, then it will.
Finally, when using a rest parameter of generic type constrained to an array type, the compiler will infer a tuple type for that parameter if possible.
Putting all that together, the above tupleOfNarrowObjects infers its argument as a tuple of objects of literal properties if possible. It is ugly (three type parameters for a single argument) but it works: you don't have to repeat yourself.
Hope that helps you. Good luck.
foois "hello" thenbaris of a type, but iffoois "goodbye", thenbaris of another type. Is that correct?