Reputation: 97247
In TypeScript, I can define the properties of a type based on a readonly tuple with the following construct:
const keys = ['A', 'B', 'C'] as const;
type T = { [key in typeof keys[number]]: boolean; };
This behaves as expected:
const t1: T = {A: true, B: true, C: true}; // OK
const t2: T = {A: true, B: true}; // Type error
Now, let's say that I want to dynamically create an object of type T
based on the keys
array, e.g. using reduce()
or ES2019's Object.fromEntries()
:
const t1: T = keys.reduce((a, v) => ({ ...a, [v]: true }), {}); // Type error
const t2: T = Object.fromEntries(keys.map(s => [s, true])); // Type error
In both cases, I need an explicit type assertion to stop the compiler from complaining:
const t1: T = keys.reduce((a, v) => ({ ...a, [v]: true }), {}) as T; // OK
const t2: T = Object.fromEntries(keys.map(s => [s, true])) as T; // OK
Can anyone suggest a way to declare and initialize an object of type T
based on the keys
array, without having to resort to an object literal or explicit type assertion?
Upvotes: 0
Views: 1621
Reputation: 51092
The simplest solution is to use Object.create(null)
instead of {}
to initialise the object. This is cheating a bit, because Object.create
has return type any
which means you don't get a type error simply because the type isn't checked.
function makeObject<K extends PropertyKey, V>(keys: K[], value: V): Record<K, V> {
const obj: Record<K, V> = Object.create(null);
for(const k of keys) {
obj[k] = value;
}
return obj;
}
The upside is that because the object has null
as its prototype, it doesn't inherit properties like 'toString'
which might mess up code which expects dynamically-accessed properties to definitely have type V
if they exist. If you explicitly do want to inherit those properties, you can use Object.create({})
instead.
Upvotes: 3
Reputation: 33091
I agree with @ABOS, that in your case it can be useful to consider augmenting of global types, however, I understand, that some times it is not an option - strict eslint rules, team code guides etc ...
I know only one solution to type correctly Array.prototype.reduce in your particular case:
const keys = ['A', 'B', 'C'] as const;
type T = { [key in typeof keys[number]]: boolean; };
const t1: T = { A: true, B: true, C: true }; // OK
const t2: T = { A: true, B: true }; // Type error
const result = keys.reduce<T>((a, v) => ({ ...a, [v]: true }), {} as T) // T
You just need to explicitly cast initial value of reduce. This problem is also related to typing of setState
method in react. See open issue here.
Regarding to Array.prototype.map
.
In your case you can type your mapping function in next way:
type Mapped<Arr extends ReadonlyArray<unknown>, Result extends ReadonlyArray<unknown> = []> =
Arr extends []
? [] : Arr extends [infer H]
? readonly [...Result, readonly [H, true]] : Arr extends readonly [infer Head, ...infer Tail]
? Mapped<readonly [...Tail], readonly [...Result, readonly [Head, true]]> : Readonly<Result>
type Result = Mapped<MyArray>; // readonly [readonly ["A", true], readonly ["B", true], readonly ["C", true], readonly ["D", true]]
/**
* Because arrays are mutable in JS,
* you are unable to type result variable as Result directly
*
* One thing you can do is to use cast operator - as
* I hope you don't mutate your arrays inside [map] callback
*/
const result = keys.map<Result[number]>((s) => ([s, true] as const)) as unknown as Result
Regarding fromEntries
.
Same as Object.keys
, fromEntries
will not return correctly typed keys of source objects, it made purposely by TS team, because, like I already said, JS mutable nature.
You have two ways here, the way which was introduced by @ABOS, or type casting - through as
operator.
For example, there is a common pattern of using Object.keys
:
const obj = { foo: 1, bar: 2 }
const keys = Object.keys(obj) as Array<keyof typeof obj> // ("foo" | "bar")[]
Please keep in mind, it is still workaround, it is not 100% type safe, because of mutable nature of JS.
Here you can find more about Object.keys
I know, you did not ask about, Object.keys, but it is still same problem as fromEntries
Summary: There are two ways: augmenting global type definition or using type casting. Each way is not completely safe, but if you are not mutating your arrays/objects it is ok to use it.
P.S. I believe there are more ways to achieve it, just because I prefer functional programming, I did not provide any example with mutable data structures, but if you are ok with mutating, @kaya3 solution is good
Upvotes: 2
Reputation: 3823
Perhaps you can consider augmenting type definition of ObjectConstructor
interface ObjectConstructor {
fromEntries<T = any, K=string|number>(entries: Iterable<readonly [PropertyKey, T]>): { [k in keyof K]: T };
}
const keys = ['A', 'B', 'C'] as const;
type T = { [key in typeof keys[number]]: boolean; };
const t2: T = Object.fromEntries<boolean, T>(keys.map(s => [s, true])); // no Type error
Upvotes: 3