I am building a FormConditions
interface where I will have an object with arbitrary keys, each of which are an instance of a class implementing a Condition
interface. I want to assign this object literal to a variable, and have the type of the resulting object A) only respond to the keys in the object literal, and B) respect any subclassing or other extending that each of those keys may have.
If you examine the code below, I have all of the actual types working just fine. The problem I'm running into is that I don't know how to assign the object to the variable directly without explicitly declaring the subtype of each key. Instead I can pass it through the identity function makeFormConditions
which uses generics to correctly infer the type of the resulting object. Is this the only way to do this or is there a way to assign it directly? Feel free to alter the definition of FormCondition
as you see fit to accomplish this.
interface Condition {
name: string
id: number
}
type FormConditions<T extends Record<string, Condition>> = {
[P in keyof T]: T[P]
}
class SimpleCondition implements Condition {
constructor(public name: string, public id: number) {}
}
class ListCondition<T> implements Condition {
constructor(public name: string, public id: number, public entries: T[]) {}
}
// This is a passthrough function just to make the types work
function makeFormConditions<T extends Record<string, Condition>>(obj: T): FormConditions<T> {
return obj;
}
// Would prefer to avoid the function call to make types work
const conditions = makeFormConditions({
simpleOne: new SimpleCondition('simpleOne', 1),
simpleTwo: new SimpleCondition('simpleTwo', 2),
list: new ListCondition('list', 3, ['foo', 'bar'])
})
// This works but is redundantly verbose
// const conditions : FormConditions<{
// simpleOne: SimpleCondition;
// simpleTwo: SimpleCondition;
// list: ListCondition<string>;
// }> = {
// simpleOne: new SimpleCondition('simpleOne', 1),
// simpleTwo: new SimpleCondition('simpleTwo', 2),
// list: new ListCondition('list', 3, ['foo', 'bar'])
// }
//
// would instead prefer to not use the function or be
// really specific about the type declaration:
// const conditions : FormConditions = {
// simpleOne: new SimpleCondition('simpleOne', 1),
// simpleTwo: new SimpleCondition('simpleTwo', 2),
// list: new ListCondition('list', 3, ['foo', 'bar'])
// }
conditions.list.name
conditions.list.entries
conditions.simpleOne.name
conditions.simpleOne.entries // error, as expected
Here's a typescript playground link to the above.
Short answer: No, you cannot assign an object literal containing heterogeneous types and maintain generic type constraints. A constrained helper function (as currently implemented) is required.
Condition
to include all Sub-type PropertiesThe definition of Condition
could be expanded to accept an optional entries
array, such that FormConditions<E, T extends Record<string, Condition<E>>>
could hold both SimpleConditions
and ListConditions
. This has the undesired side-effect that instances of SimpleCondition
may reference the missing entries
property without error.
interface Condition<E> {
name: string
id: number
entries?: E[]
}
type FormConditions<E, T extends Record<string, Condition<E>>> = {
[P in keyof T]: T[P]
}
class SimpleCondition<E = never> implements Condition<E> {
constructor(public name: string, public id: number) {}
}
class ListCondition<E> implements Condition<E> {
constructor(public name: string, public id: number, public entries: E[]) {}
}
const conditions: FormConditions<string, Record<string, Condition<string>>> = {
simpleOne: new SimpleCondition('simpleOne', 1),
simpleTwo: new SimpleCondition('simpleTwo', 2),
list: new ListCondition('list', 3, ['foo', 'bar'])
}
conditions.list.name;
conditions.list.entries;
conditions.simpleOne.name;
conditions.simpleOne.entries; // Expected error; however, no error, since `entries` is optional parameter.
Condition
to include only name
and id
Since Condition
is limited, there is an error (as expected) when attempting to access entries
on instances of SimpleCondition
. However, in the context of type FormConditions<E, T extends Record<string, Condition>>
, instances of ListCondition
result in an error when referring to entries
, since the type has been narrowed to Condition
.
interface Condition {
name: string
id: number
}
type FormConditions<E, T extends Record<string, Condition>> = {
[P in keyof T]: T[P]
}
class SimpleCondition<E = never> implements Condition {
constructor(public name: string, public id: number) {}
}
class ListCondition<E> implements Condition {
constructor(public name: string, public id: number, public entries: E[]) {}
}
const conditions: FormConditions<string, Record<string, Condition>> = {
simpleOne: new SimpleCondition('simpleOne', 1),
simpleTwo: new SimpleCondition('simpleTwo', 2),
list: new ListCondition('list', 3, ['foo', 'bar'])
}
conditions.list.name;
conditions.list.entries; // Error: Property 'entries' does not exist on type 'Condition'.
conditions.simpleOne.name;
conditions.simpleOne.entries; // error (as expected - Good)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With