我有一个库,可导出类似于以下内容的实用程序类型:
type Action<Model extends object> = (data: State<Model>) => State<Model>;
此实用程序类型允许您声明一个将作为“动作”执行的功能。它收到一个通用参数,即操作将针对的Model
。
然后使用导出的其他实用程序类型键入“操作”的data
参数;
type State<Model extends object> = Omit<Model, KeysOfType<Model, Action<any>>>;
State
实用程序类型基本上采用传入的Model
泛型,然后创建一个新类型,其中所有类型为Action
的属性都已删除。
例如这是上面的基本用户土地实现;
interface MyModel {
counter: number;
increment: Action<Model>;
}
const myModel = {
counter: 0,
increment: (data) => {
data.counter; // Exists and typed as `number`
data.increment; // Does not exist, as stripped off by State utility
return data;
}
}
上面的效果很好。 ?
但是,在某些情况下,特别是在定义了通用模型定义时,还有工厂函数来产生通用模型的实例时,我都会感到困惑。
例如;
interface MyModel<T> {
value: T; // ? a generic property
doSomething: Action<MyModel<T>>;
}
function modelFactory<T>(value: T): MyModel<T> {
return {
value,
doSomething: data => {
data.value; // Does not exist ?
data.doSomething; // Does not exist ?
return data;
}
};
}
在上面的示例中,我希望在data
操作已被删除的地方键入doSomething
参数,并且通用value
属性仍然存在。但是,情况并非如此-value
工具的State
属性也已删除。
我认为,原因是T
是通用的,没有对其施加任何类型限制/缩小,因此类型系统确定它与Action
类型相交并随后将其删除来自data
参数类型。
有没有办法解决这个限制?我已经进行了一些研究,希望能有某种机制可以说明T
是Action
的任何 except 。即否定类型限制。
想象:
function modelFactory<T extends any except Action<any>>(value: T): UserDefinedModel<T> {
但是TypeScript不存在该功能。
有人知道我可以像预期的那样工作吗?
此处提供了完整的代码段,以帮助调试:
// Returns the keys of an object that match the given type(s)
type KeysOfType<A extends object, B> = {
[K in keyof A]-?: A[K] extends B ? K : never
}[keyof A];
// Filters out an object, removing any key/values that are of Action<any> type
type State<Model extends object> = Omit<Model, KeysOfType<Model, Action<any>>>;
// My utility function.
type Action<Model extends object> = (data: State<Model>) => State<Model>;
interface MyModel<T> {
value: T; // ? a generic property
doSomething: Action<MyModel<T>>;
}
function modelFactory<T>(value: T): MyModel<T> {
return {
value,
doSomething: data => {
data.value; // Does not exist ?
data.doSomething; // Does not exist ?
return data;
}
};
}
您可以在此处使用此代码示例: https://codesandbox.io/s/reverent-star-m4sdb?fontsize=14
答案 0 :(得分:7)
这是一个有趣的问题。对于条件类型中的泛型类型参数,Typescript通常不能做太多事情。如果发现对extends
的评估涉及类型参数,它只会推迟对它的评估。
如果我们可以让打字稿使用一种特殊的类型关系,即平等关系(而不是扩展关系),则适用例外。对于编译器而言,相等关系很容易理解,因此无需推迟条件类型评估。通用约束是编译器中使用类型相等的少数几个位置之一。让我们看一个例子:
function m<T, K>() {
type Bad = T extends T ? "YES" : "NO" // unresolvable in ts, still T extends T ? "YES" : "NO"
// Generic type constrains are compared using type equality, so this can be resolved inside the function
type Good = (<U extends T>() => U) extends (<U extends T>() => U) ? "YES" : "NO" // "YES"
// If the types are not equal it is still un-resolvable, as K may still be the same as T
type Meh = (<U extends T>()=> U) extends (<U extends K>()=> U) ? "YES": "NO"
}
我们可以利用这种行为来识别特定类型。现在,这将是完全类型匹配,而不是扩展匹配,并且完全类型匹配并不总是适合。但是,由于Action
只是一个函数签名,所以精确的类型匹配可能就足够好了。
让我们看看是否可以提取与更简单的函数签名匹配的类型,例如(v: T) => void
:
interface Model<T> {
value: T,
other: string
action: (v: T) => void
}
type Identical<T, TTest, TTrue, TFalse> =
((<U extends T>(o: U) => void) extends (<U extends TTest>(o: U) => void) ? TTrue : TFalse);
function m<T>() {
type M = Model<T>
type KeysOfIdenticalType = {
[K in keyof M]: Identical<M[K], (v: T) => void, never, K>
}
// Resolved to
// type KeysOfIdenticalType = {
// value: Identical<T, (v: T) => void, never, "value">;
// other: "other";
// action: never;
// }
}
以上类型KeysOfIdenticalType
与我们过滤所需的类型很接近。对于other
,保留属性名称。对于action
,属性名称被删除。 value
周围只有一个令人讨厌的问题。由于value
的类型为T
,因此无法轻易解决T
和(v: T) => void
不相同(实际上可能不相同)的问题。
我们仍然可以确定value
与T
相同:对于类型T
的属性,将(v: T) => void
的此检查与never
相交。与never
的任何交集都可以简单地解析为never
。然后,我们可以使用另一种身份检查来添加类型为T
的属性:
interface Model<T> {
value: T,
other: string
action: (v: T) => void
}
type Identical<T, TTest, TTrue, TFalse> =
((<U extends T>(o: U) => void) extends (<U extends TTest>(o: U) => void) ? TTrue : TFalse);
function m<T>() {
type M = Model<T>
type KeysOfIdenticalType = {
[K in keyof M]:
(Identical<M[K], (v: T) => void, never, K> & Identical<M[K], T, never, K>) // Identical<M[K], T, never, K> will be never is the type is T and this whole line will evaluate to never
| Identical<M[K], T, K, never> // add back any properties of type T
}
// Resolved to
// type KeysOfIdenticalType = {
// value: "value";
// other: "other";
// action: never;
// }
}
最终的解决方案如下所示:
// Filters out an object, removing any key/values that are of Action<any> type
type State<Model extends object, G = unknown> = Pick<Model, {
[P in keyof Model]:
(Identical<Model[P], Action<Model, G>, never, P> & Identical<Model[P], G, never, P>)
| Identical<Model[P], G, P, never>
}[keyof Model]>;
// My utility function.
type Action<Model extends object, G = unknown> = (data: State<Model, G>) => State<Model, G>;
type Identical<T, TTest, TTrue, TFalse> =
((<U extends T>(o: U) => void) extends (<U extends TTest>(o: U) => void) ? TTrue : TFalse);
interface MyModel<T> {
value: T; // ? a generic property
str: string;
doSomething: Action<MyModel<T>, T>;
method() : void
}
function modelFactory<T>(value: T): MyModel<T> {
return {
value,
str: "",
method() {
},
doSomething: data => {
data.value; // ok
data.str //ok
data.method() // ok
data.doSomething; // Does not exist ?
return data;
}
};
}
/// Still works for simple types
interface MyModelSimple {
value: string;
str: string;
doSomething: Action<MyModelSimple>;
}
function modelFactory2(value: string): MyModelSimple {
return {
value,
str: "",
doSomething: data => {
data.value; // Ok
data.str
data.doSomething; // Does not exist ?
return data;
}
};
}
注释:这里的限制是,它仅适用于一个类型参数(尽管可以将其改编为更多类型)。而且,该API对于任何使用者而言都有些混乱,因此这可能不是最佳解决方案。可能有一些我尚未发现的问题。如果有找到,让我知道?
答案 1 :(得分:2)
如果我能说T不是Action类型的话,那将是很棒的。延伸的倒数
就像您说的那样,问题是我们还没有负面约束。我也希望他们能尽快获得这种功能。在等待时,我提出了这样的解决方法:
type KeysOfNonType<A extends object, B> = {
[K in keyof A]-?: A[K] extends B ? never : K
}[keyof A];
// CHANGE: use `Pick` instead of `Omit` here.
type State<Model extends object> = Pick<Model, KeysOfNonType<Model, Action<any>>>;
type Action<Model extends object> = (data: State<Model>) => State<Model>;
interface MyModel<T> {
value: T;
doSomething: Action<MyModel<T>>;
}
function modelFactory<T>(value: T): MyModel<T> {
return {
value,
doSomething: data => {
data.value; // Now it does exist ?
data.doSomething; // Does not exist ?
return data;
}
} as MyModel<any>; // <-- Magic!
// since `T` has yet to be known
// it literally can be anything
}
答案 2 :(得分:1)
count
和value
将始终使编译器不满意。要解决此问题,您可以尝试执行以下操作:
{
value,
count: 1,
transform: (data: Partial<Thing<T>>) => {
...
}
}
由于使用的是Partial
实用程序类型,因此在没有transform
方法的情况下您会没事的。
答案 3 :(得分:1)
通常,我读过两次,但并不完全了解您想要实现的目标。根据我的理解,您想从精确分配给interface Thing<T> {
value: T;
count: number;
transform: (data: Omit<Thing<T>, 'transform'>) => void; // here the argument type is Thing without transform
}
// ? the factory function accepting the generic
function makeThing<T>(value: T): Thing<T> {
return {
value,
count: 1,
transform: data => {
data.count; // exist
data.value; // exist
},
};
}
的类型中省略@functions
。要做到这一点很简单,我们需要使用Omit:
@functions
{
async System.Threading.Tasks.Task RenderArea(Area area)
{
<tr>
<td class="@area.CssClass">
<p class="compact">
<a href="/Area/@area.Id" class="font-weight-bold">@area.Title</a>
@if (!string.IsNullOrWhiteSpace(area.Description))
{
<br />@area.Description
}
</p>
</td>
<td class="@area.CssClass">
<img src="~/images/Edit.png" class="edit-area button-img" data-id="@area.Id" title="Edit" />
<img src="~/images/Delete.png" class="delete-area button-img" data-id="@area.Id" title="Delete" />
</td>
</tr>
}
}
由于您在其他实用程序类型中提供了相当复杂的功能,因此不确定这是否是您想要的。希望对您有所帮助。