Issue
I am trying to write types for an existing API. A minimal example of the kind of type relationship I'm trying to encode is shown below.
const res1: res1Type = [
{
name: "FieldNameA",
value: 123
},
{
name: "FieldNameB",
value: 123
}
];
const res2: res2Type = {
AlwaysPresent1: "test",
AlwaysPresent2: 1,
FieldNameA: {
field1: 1,
field2: 2
},
FieldNameB: {
field1: 1,
field2: 2
}
};
type res1Type = {
name: string;
value: number;
}[];
type res2Type = {
AlwaysPresent1: string;
AlwaysPresent2: number;
[k: string]: subType | string | number;
};
type subType = {
field1: number;
field2: number;
};
res1 is an array of arbitrary length and the name field from each entry in the array of objects maps to a field in res2 (which has a few extra fields that are always present). These mapped fields have an object property. I've managed to used an indexed signature to describe these types but this requires me to make it a Union type:
[k: string]: subType | string | number;
Is there a way to more explicitly map the fields in res1 to fields in res2? Is there an alternative to index signatures which allow me to define an arbitrary amount of string:subType fields?
UPDATE
Implemented the approach suggested by jcalz with a small change to explicitly make res1Type Readonly.
type Res1Type = Readonly<{
name: string;
value: number;
}[]>;
Solution
The only way for Res2Type to know about the particular literal types of name properties of the elements of res1 is for you to make it generic in the type of res1 or some related type. Here's one way to write it:
type Res2Type<T extends Res1Type> = {
AlwaysPresent1: string;
AlwaysPresent2: number;
} & Record<T[number]["name"], SubType>
So we want res2 to be of type Res2Type<typeof res1> using the TS typeof operator:
But we need to be careful. If you just annotate res1 as being of type Res1Type, like
const res1: Res1Type = [ ⋯ ];
then the type of that variable will be widened to Res1Type and forget anything specific about the initializer expression. And that would mean that res2 would be of type Res2Type<Res1Type> looks like { AP1: string; AP2: string} & {[k: string]: SubType }, an intersection type which is hard to use (see How to define Typescript type as a dictionary of strings but with one numeric "id" property for a discussion about such a type) and doesn't actually constrain the keys of res2 at all.
Instead of annotating, we should use the satisfies operator to checking the inferred type of the initializer against Res1Type without widening it:
const res1 = [ ⋯ ] satisfies Res1Type;
Except we still need to be careful, because the inferred type of the name properties will still be string. An object like {name: "xxx"} is inferred to be of type {name: string} and not {name: "xxx"}. If we want the latter we need to give the compiler a hint that we care about the literal types. The easiest way is to use a const assertion on the expression:
const res1 = [ ⋯ ] as const satisfies Res1Type;
And that is enough to make it work.
So, let's see what we get:
const res1 = [
{
name: "FieldNameA",
value: 123
},
{
name: "FieldNameB",
value: 123
}
] as const satisfies Res1Type;
/* const res1: [{
readonly name: "FieldNameA";
readonly value: 123;
}, {
readonly name: "FieldNameB";
readonly value: 123;
}] */
That compiles without error (in TS5.3+) and you can see it keeps track of "FieldNameA" and "FieldNameB". So the type we want for res2 is
type R2Test = Res2Type<typeof res1>;
/* type R2Test = {
AlwaysPresent1: string;
AlwaysPresent2: number;
} & Record<"FieldNameA" | "FieldNameB", SubType> */
If you want you can annotate res2 with that type:
const res2: Res2Type<typeof res1> = {
AlwaysPresent1: "test",
AlwaysPresent2: 1,
FieldNameA: {
field1: 1,
field2: 2
},
FieldNameB: {
field1: 1,
field2: 2
}
}; // okay
or you could continue using as const satisfies instead, depending on your later needs:
const res2 = {
AlwaysPresent1: "test",
AlwaysPresent2: 1,
FieldNameA: {
field1: 1,
field2: 2
},
FieldNameB: {
field1: 1,
field2: 2
}
} as const satisfies Res2Type<typeof res1>; // okay
Res2Type
type Res1Type = {
name: string;
value: number;
}[];
const res1 = [
{
name: "FieldNameA",
value: 123
},
{
name: "FieldNameB",
value: 123
}
] as const satisfies Res1Type;
type SubType = {
field1: number;
field2: number;
};
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.