While this solution may not be flawless, the updated
type appears to be accurate:
type First<T extends string> = T extends `${infer L}.${string}` ? L : T
type Nested<T extends string> = T extends `${string}.${infer R}` ? R : string
type _ConstructedType<T extends string> = string extends Nested<T> ? string : {
[Key in T as First<Nested<T>>]: _ConstructedType<Nested<T>>
}
type ConstructedType<K extends readonly string[]> = {
[Key in K[number] as First<Key>]: _ConstructedType<Key>
}
function createConf<K extends readonly string[]>(conf: {items: K, onChange: (updated: ConstructedType<K>) => any}) {
return conf
}
createConf({
items: ['id', 'nested.id', 'nested.name'] as const,
onChange: updated => {
console.log(`You updated ${updated.nested.name}(id: ${updated.nested.id})`);
},
})
In your inquiry, you indicated the desire for a MyConfiguration
type. However, a type on its own cannot enforce property constraints. Thus, a factory function named createConf
is implemented to address this. By passing a conf
object to the function, all types are inferred accordingly.
An existing limitation that remains unresolved involves appending as const
after the items
array. Failing to do so will result in TypeScript inferring the incorrect type as string[]
instead of a tuple.
Playground
Credit goes to @jcalz for correcting various issues with the code:
type ConstructedType<K extends string> = {
[P in K as P extends `${infer L}.${string}` ? L : P]:
[P] extends [`${string}.${infer R}`] ? ConstructedType<R> : string;
}
function createConf<K extends string>(conf:
{ items: readonly K[], onChange: (updated: ConstructedType<K>) => any }) {
return conf
}
type Test = ConstructedType<'id' | 'nested.id' | 'nested.child.name'>
const x = createConf({
items: ['id', 'nested.id', 'nested.name', 'nested.child.name'],
onChange: updated => {
console.log(`You updated ${updated.nested.name}(id: ${updated.nested.id})`);
},
})
An alternative solution provided by @jcalz can be found here.