I have an object like this (demo in ts playground):

const sites = {
    stack: {url: 'https://stackoverflow.com/'},
    google: {url: 'https://www.google.com/'},
    azure: {url: 'https://portal.azure.com/'}
} as const

What I'd like to do is create a union type with all the keys that were used, which I can do like this:

type SiteNames = keyof typeof sites; // "stack" | "google" | "azure"

However, I'd also like to add type saftey to the sites intialization where all of the object values are of a certain type like this (demo in ts playground):

interface ISiteDetails {
    url: string;
}

const sites: Record<string, ISiteDetails> = {
    stackoverflow: {url: 'https://stackoverflow.com/'},
    google: {url: 'https://www.google.com/'},
    azure: {url: 'https://portal.azure.com/'}
} as const

This provides some type checking when creating sites, but also removes the const assertion from the final type, so now SiteNames just resolves to a string:

type SiteNames = keyof typeof sites; // string

Question: Is there any way to have both? Strong typing when creating a Record<any, ISiteDetails but also the ability to extract all the object keys into a new union type?

Workaround: not as ergonomic, but I could add a final layer of type checking by reassigning site to an exported variable like this (demo in ts playground):

const SitesTyped: Record<SiteNames, ISiteDetails> = sites;

This is generally done through an identity function. This will allow you to constrain your inputs while still using their specific types like this (demo in ts playground):

function defineSites<T extends Record<string, ISiteDetails>>(template: T) {
    return template;
}

const sites = defineSites({
    stackoverflow: {url: 'https://stackoverflow.com/'},
    google: {url: 'https://www.google.com/'},
    azure: {url: 'https://portal.azure.com/'}
})

I have a little one-liner that I import sometimes. It's higher order identity.

export const HOI = <Constraint> () => <T extends Constraint> (definition: T) => definition;

export const defineSites = HOI<Record<string, ISiteDetails>>();
// use as normal

You may wish to write this as a function if you need it in a .tsx file

export function HOI<Constraint>() { 
    return () => <T extends Constraint> (definition: T) => definition;
}