我正在尝试在打字稿中创建带有漂亮的DX的路由器初始化程序。
我的目标是允许开发人员在每个级别为每个路线的参数定义路径和参数类型。为了更好地说明我的问题,我的示例被截断了。
每个路由节点如下:
export type RouteArgs<TProps extends t.Props> =
{
path: string;
type?: TProps;
};
为了保留类型,但强制执行类型约束,我创建了一个“构建器”功能:
export type RoutesArgs = {
[key: string]: RouteArgs<{}>;
};
interface RoutesFn {
<TRoutesArgs extends RoutesArgs>(args: TRoutesArgs): TRoutesArgs;
}
export const routes: RoutesFn = args => args;
routes
函数的唯一用途是强制实参的类型,并返回从实参推断出的实际类型。它的用法如下:
const r = routes({
FOO: {
// Intellisense kicks in here...
},
});
当智能感知加入FOO
对象时,它将同时建议path
和type
属性。 path
是必需的,因此我添加了路径,然后再次点击“智能感知”。
const r = routes({
FOO: {
path: '/foo',
// Now, intellisense is empty...
},
});
它并没有像建议的那样仅推荐type
,现在却没有任何建议。
实际上,它还允许我添加“垃圾”属性...
const r = routes({
FOO: {
path: '/foo',
// Ideally, it would complain about the following...
rubbish: 1234
},
});
我担心我会遇到打字稿的结构化打字功能。我认为它对path
感到满意,然后放弃了与其余匹配。
这种方法的魅力之一在于它只是一个对象常量,我不必为每个路由节点实例化类,因此我想避免使用类。
在这种情况下,是否有任何方法可以说服打字稿强制实施名义打字,而无需为每条路线采用传递功能?
答案 0 :(得分:1)
我无法重现您使用IntelliSense遇到的问题。
对于禁止额外的属性,您是正确的,默认情况下结构类型系统允许此类额外的属性。 (具体来说,interface
和class
扩展名是必需的...如果向子类或接口扩展名中添加属性导致与超类或父接口不兼容,则继承和子类型将很笨拙。)对象类型通常不会将TypeScript中的“。exact”视为。我之所以说“一般”,是因为该语言在某些情况下使用excess property checking为对象文字确定了一个例外。但是,在您的情况下这并不适用,因为对象文字是扩展对象类型的通用类型,并且由于扩展可能具有额外的属性,因此您回到了起点。 >
因此TypeScript不直接支持确切的类型 。但是您可以indirectly describe such types via generic type constraints。例如:
type Exactly<T, C> = T & { [K in Exclude<keyof C, keyof T>]: never };
interface RoutesFn {
<T extends RoutesArgs>(args: T & { [K in keyof T]: Exactly<RouteArgs<{}>, T[K]> }): T;
}
Exactly<T, C>
通用类型采用类型T
和候选类型C
,并返回与C
兼容的新类型当且仅当它与T
“完全”匹配时。如果C
包含任何额外的属性,则这些额外的属性将转换为类型never
。
然后RoutesFn
类型与之前类似,不同之处在于,args
参数是针对两个受约束的通用T
进行检查的(我将其从TRoutesArgs
缩短了) 和版本,其中已根据T
的确切版本检查了RouteArgs<{}>
的每个属性。
让我们看看它是否有效
const r = routes({
FOO: {
path: '/foo',
type: { f: 1 },
rubbish: 1 // error!
//~~~~~ <-- Type 'number' is not assignable to type 'never'
},
BAR: {
path: '/bar',
}
});
那很好;您现在会在不丢失类型推断的情况下在其他属性上出现错误。至少在我的IDE中还存在IntelliSense(请参阅image)。
好的,希望能有所帮助;祝你好运!