Annotating a variable as Foo throws away the key information you need
The main obstacle here is that when you annotate a variable with a (non-union) type, like const example: Foo = ..., the compiler will treat that variable as being of that type regardless of what value you initialize it with. And so any more specific information about the initialized value, such as the string literal type "mykey1", will be thrown away. The definition of Foo has just string as the key property of the elements of the one array, and so string is what you'll get if you try to inspect the variable's type:
const example: Foo = {
one: [{ key: "mykey1" }, { key: "mykey2" }, { key: "mykey3" }]
};
// const example: Foo
type KeyType = typeof example["one"][number]["key"]
// type KeyType = string, oops
So you really don't want to annotate example (unless you want to manually write out the full type with all the information needed). Instead you should try to get the compiler to infer the type of example in such a way that it has the information you care about, while still being seen as assignable to Foo.
Simply omitting the annotation also throws this key information away
If we simply leave off the annotation, that's also not sufficient:
const example = {
one: [{ key: "mykey1" }, { key: "mykey2" }, { key: "mykey3" }]
};
// const example: { one: { key: string; }[]; }
type KeyType = typeof example["one"][number]["key"]
// type KeyType = string, oops
Again, it's just string. That's because the default heuristics for inferring variable types tends to widen any string literal properties to just string. Properties are sometimes reassigned, and the compiler has no idea that you don't intend to write example.one[0].key = "somethingElse". And so string is the type it infers instead of "mykey1". We need to do something else.
A const assertion will preserve the key information, but it's not compatible with Foo anymore
If you want to tell the compiler that you are not going to modify the contents of example and that it should try to infer the most specific type possible, you can use a const assertion on the initializer:
const example = {
one: [{ key: "mykey1" }, { key: "mykey2" }, { key: "mykey3" }]
} as const;
/* const example: { readonly one: readonly [
{ readonly key: "mykey1"; },
{ readonly key: "mykey2"; },
{ readonly key: "mykey3"; }
];
*/
type KeyType = typeof example["one"][number]["key"]
// type KeyType = "mykey1" | "mykey2" | "mykey3", hooray!
Now example is inferred to be an object with a readonly property containing a readonly array of objects with readonly properties whose values are the string literals we care about. And so KeyType is exactly the union of string literals we want.
That's great, and we'd be done here, except for one wrinkle. The inferred type of example is not assignable to your Foo as defined. It turns out that readonly arrays are not assignable to mutable arrays (it is the other way around), and so this happens:
function acceptFoo(foo: Foo) { }
acceptFoo(example); // error! one is readonly
// -----> ~~~~~~~
Maybe you should redefine Foo to be compatible with a const assertion
Are you ever going to add or remove elements from a Foo's one property? If not, then the easiest solution here is to just redefine Foo:
interface Foo {
one: readonly { // <-- change to readonly array type
key: string;
// more properties
}[]
}
acceptFoo(example); // okay now
If you don't want to make that change, then you need some other solution. And even if you do, leaving off the annotation and using as const has the side effect that example really might not be a valid Foo, and you wouldn't catch it until you tried to use it as a Foo later:
const example = {
one: [{ key: "mykey1" }, { key: "mykey2" }, { kee: "mykey3" }]
} as const; // no error here
/* const example: { readonly one: readonly [
{ readonly key: "mykey1"; },
{ readonly key: "mykey2"; },
{ readonly kee: "mykey3"; }
];
*/
acceptFoo(example); // error here
The third element of one has a kee property instead of a key property. That's a mistake, but there's no error at the const example = ... line because nothing says it has to be a Foo. You get an error later when you treat it like a Foo.
This might be acceptable to you, or maybe not. If it is, then we can stop here. If not, read on:
Or you can make a generic helper function to infer a Foo-compatible type that also preserves key information
Another idea instead of using as const is to make a generic helper function that guides the inference process. At runtime it would just return its input, so it looks like a no-op. Here's one way to do it:
interface FooWithKeys<K extends string> extends Foo {
one: {
key: K;
// more properties
}[]
}
const asFoo = <K extends string>(fwk: FooWithKeys<K>) => fwk;
The FooWithKeys<K> type is an extension of Foo where the keys in one are known to be K, which is constrained to be a subtype of string. A type FooWithKeys<"a" | "b"> is assignable to Foo, but the compiler knows that the keys are a or b and not just string.
The asFoo() helper function will look at its input and infer a type for K that is consistent with it. Since the type parameter K is constrained to string, the compiler will try to infer string literal types for it if possible.
Let's see it in action:
const example = asFoo({
one: [{ key: "mykey1" }, { key: "mykey2" }, { key: "mykey3" }]
})
// const example: FooWithKeys<"mykey1" | "mykey2" | "mykey3">
Looks good. Now we can get the key type as before:
type KeyType = typeof example["one"][number]["key"]
// type KeyType = "mykey1" | "mykey2" | "mykey3", hooray!
and make your A dictionary:
const A = {} as Record<KeyType, string>;
example.one.map(el => {
A[el.key] = "anything"
})
And example is still seen as a Foo:
acceptFoo(example); // okay
And if we made any mistake with example, we'd get an error right there:
const example = asFoo({
one: [{ key: "mykey1" }, { key: "mykey2" }, { kee: "mykey3" }] // error!
// -----------------------------------------> ~~~~~~~~~~~~~
// Object literal may only specify known properties, and 'kee' does not exist in type
})
Playground link to code
exampleasFooand then ask the compiler to remember the string literal values fromexample.one[someNumber].key; that's juststringin the definition ofFoo. If you want the compiler to keep track of it, you needexampleto be of a type which represents those string literals, and then you can useRecord<typeof example["one"][number]["key"], string>like this. If this works for you I'll write up an answer. If not, please elaborate on what's missing.asFoofunction. If you write this answer explaining the same would be helpful 😊