Reputation: 1053
New to Typescript, I have a quick question about typings an array of object. Right now if I do :
const my_array = [{
foo: "hello",
bar: "Typescript"
},
{
foo: "goodbye",
bar: "JavaScript"
}
];
It will by default infer my_array with a type of
{foo:string; bar:string;}[]
My request : I would like to have typings more accurate such as:
{foo: "hello" | "goodbye"; bar: // value according to foo }
I also would like to have a DRY solution and define my foo
and bar
values only once for better maintainability
Upvotes: 0
Views: 66
Reputation: 2187
Current Typescript implementation from version 3.7.5, provides limited support for deducing compile-time constant narrow argument types, but specific construction allows it:
type Narrowable = "" | 0 | true | false | symbol | object | undefined | void | null | {} | [Narrowable] | { [k in keyof any]: Narrowable }
function gen<T extends Narrowable>(x: T): T { return x };
const f = gen(["www", 1,2,3,[1,2,3,[5,"qqq"],"str"],4, {a:5,b:{c:{a:54,b:"www"}}}])
Upvotes: 0
Reputation: 1411
You can define types for each type of record:
type Alpha = {"foo": "alpha", "bar": number}
type Beta = {"foo": "beta", "bar": string}
Then you can define an array which is a list of either Alpha
s or Beta
s (called a union type)
let arr: (Alpha | Beta)[] = some_array();
When iterating over the array, TypeScript will know the types of the fields as
for (let el of arr) {
// el.foo is "alpha" | "beta"
// el.bar is number | string
But if you check the .foo
tag, TypeScript will narrow the type of .bar
, because it actually knows each element is either a Alpha
or Beta
:
if (el.foo == "alpha") {
// el.bar is number
} else {
// el.bar is string
}
}
Upvotes: 2
Reputation: 329943
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.
Upvotes: 2