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.