Why autocomplete stop working in an object with type in TypeScript?

Viewed 6235

I have a list of routes in one object and want to import it in other file and have autocomplete for object properties.

index.ts

import allRoutes from './routes';

allRoutes.routeHome;

routes.ts

const allRoutes = {
  routeHome: '',
  routePortfolio: 'portfolio'
};

export default allRoutes; 

All works fine. But if I add types to my allRoutes for some typecheking like this:

const allRoutes: {[key: string]:string} = {
  routeHome: '',
  routePortfolio: 'portfolio'
};

or like this:

interface IRoutes {
    [key: string]: string;
}

const allRoutes: IRoutes = {
    routeHome: '',
    routePortfolio: 'portfolio'
};

Everything breaks down

I try this in WebStorm or VSCode. If I add a type for object properties - autocomplete stops working. Why does it happen? And how I can fix this?

8 Answers

Once you initialize the constant with type { [key: string]: string }, the original type is lost. If you want to preserve the original type but check that it is assignable to { [key: string]: string }, you can do this:

function asStrings<T extends { [key: string]: string }>(arg: T): T {
  return arg;
}

const allRoutes = asStrings({
  routeHome: '',
  routePortfolio: 'portfolio'
});

There is a suggestion for a solution that would not require calling a function.

Logged as WEB-34642, please follow it for updates

If you want to type your object without autocomplete loss and keep advantage of readonly you can use as const

const object = {
  title: "Autocomplete",
} as const

How about this solution?

interface IRoute {
    name: string;
    destination: string;
}

class AllRoutes {
    public static readonly Home: IRoute = { name: "Home", destination: "" }; 
    public static readonly Portfolio: IRoute = { name: "Portfolio", destination: "portfolio" }; 
}
type RouteType = 'routeHome' | 'routePortfolio';

export const allRoutes: Record<RouteType, string> = {
  routeHome: '',
  routePortfolio: 'portfolio'
};

This should give the autocomplete.

This is what helped me to have autocompletion and prevent typescript from moaning

type TPage = {
    id: string;
    title: string;
    path: string;
};

function asRecord<T extends Record<string, TPage>>(arg: T): T & Record<string, TPage> {
    return arg;
}

const EXAMPLES_PAGES = asRecord({
    chart2D_basicCharts_BandSeriesChart: {
        id: "chart2D_basicCharts_BandSeriesChart",
        title: "BandSeriesChart",
        path: "/chart2D_basicCharts_BandSeriesChart"
    },
    chart2D_basicCharts_FanChart: {
        id: "chart2D_basicCharts_FanChart",
        title: "FanChart",
        path: "/chart2D_basicCharts_FanChart"
    }
})

const currentExampleKey = Object.keys(EXAMPLES_PAGES).find(key => EXAMPLES_PAGES[key].path === location.pathname);

Generic solution for arbitrary type T (eg. enforce { [key: string]: T } )

If you don't know the type in advance it's a little trickier to get this working. You need a factory function to create yourself a 'helper' function to enforce the type.

This is the best I could come up with - I think anything beyond this will require language changes:

export const AssertRecordType = <T>() => <D extends Record<string, T>>(d: D) => d;

type Dog = {
   breed: string
};

// Note the double function call
const dogs = AssertRecordType<Dog>()(
{
    abbie: { breed: 'Black labrador' },
    kim: { breed: 'Jack russel mix' }
});

// typing dogs.k will suggest kim

If you need to do this a lot you can create an alias for each one (you may want to rename the function to indicate it's a factory function CreateRecordTypeAsserter ?):

export const AssertStringRecord = AssertRecordType<string>();
export const AssertDogRecord = AssertRecordType<Dog>();

You can then use these anywhere with just a single function call which is ugly:

const dogs = AssertDogRecord({ bryn: { breed: 'Collie' } });
// dogs.b will suggest bryn

BTW: Don't get hung up on the function call, even if it's called a lot it's literally just returning the value that was passed in.

If you're still unclear what's happening, the expanded form may be easier to read:

// create a helper function to assert the type of a Record
export const AssertRecordType = <T>() => 
{
    // returns helper function using the 'captured' T
    return <D extends Record<string, T>>(d: D) => 
    {
        // return the original value, the function is just used to 
        // enforce the type
        return d;
    };
};
interface IRoutes {
    [key: string]: string;
}

export const allRoutes = {
  routeHome: '',
  routePortfolio: 'portfolio'
};

// Make TS check allRoutes, but leave it untyped to make autocomplete work.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const test: IRoutes = allRoutes;

This looks horrible and also gives no autocomplete inside allRoutes. I would not recommend this approach.

Related