为什么自动完成停止在 TypeScript 类型的对象中工作?

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

我在一个对象中有一个路由列表,想将其导入其他文件并自动完成对象属性。

index.ts

import allRoutes from './routes';

allRoutes.routeHome;

routes.ts

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

export default allRoutes; 

一切正常。但是,如果我将类型添加到我的 allRoutes 中以进行这样的类型检查:

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

或者像这样:

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

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

一切都崩溃了

我在 WebStorm 或 VSCode 中尝试这个。如果我为对象属性添加类型 - 自动完成将停止工作。 为什么会这样?我该如何解决这个问题?

记录为WEB-34642,更新请关注

一旦用 { [key: string]: string } 类型初始化常量,原始类型就会丢失。如果您想保留原始类型但检查它是否可分配给 { [key: string]: string },您可以这样做:

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

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

有一个 suggestion 不需要调用函数的解决方案。

这个解决方案怎么样?

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" }; 
}
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;

这看起来很糟糕,而且 allRoutes 中也没有自动完成功能。我不推荐这种方法。

type RouteType = 'routeHome' | 'routePortfolio';

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

这应该会自动完成。

这就是帮助我实现自动完成并防止打字稿抱怨的原因

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);

任意类型 T 的通用解决方案(例如强制 { [key: string]: T }

如果您事先不知道类型,那么让它工作起来会有点棘手。您需要一个工厂函数来为自己创建一个 'helper' 函数来强制执行类型。

这是我能想到的最好的 - 我认为超出这个范围的任何事情都需要更改语言:

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

如果您经常需要这样做,您可以为每个函数创建一个别名(您可能想重命名该函数以表明它是一个工厂函数 CreateRecordTypeAsserter?):

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

然后你可以在任何地方使用它们,只需要一个丑陋的函数调用:

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

顺便说一句:不要挂断函数调用,即使它被调用很多,它实际上只是返回传入的值。

如果您仍然不清楚发生了什么,展开的表格可能更容易阅读:

// 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;
    };
};

如果你想在不丢失自动完成的情况下键入你的对象并保持只读的优势,你可以使用 as const

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