This solution might not be perfect but the type of updated
seems to be correct:
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 question you specified that you want to have a type
called MyConfiguration
. A type
alone cannot enforce any constraints between properties. So we have to create a factory function called createConf
. We can now pass a conf
object to createConf
and all types are inferred.
A drawback that I don't know how to fix yet, is that you have to write as const
behind the items
array. Otherwise TypeScript will infer the type as string[]
and not as a tuple.
Playground
Thanks to @jcalz for this version which fixes the issues:
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})`);
},
})
He also provided an alternative solution.