类型映射时的通用属性问题


11

我有一个库,可导出类似于以下内容的实用程序类型:

type Action<Model extends object> = (data: State<Model>) => State<Model>;

此实用程序类型使您可以声明将作为“动作”执行的功能。它收到一个通用的论据,Model即该操作将针对的论点。

data然后使用导出的另一个实用程序类型键入“ action” 的参数。

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

在上面的示例中,我希望在datadoSomething删除操作的地方键入自变量,并且通用value属性仍然存在。但是,情况并非如此-该value属性也已被我们的State实用程序删除。

我相信这是因为它T是通用的,没有对其施加任何类型限制/缩小,因此类型系统决定它与一个Action类型相交,然后将其从data参数类型中删除。

有办法解决这个限制吗?我已经做了一些研究,希望能有一些机制可以说明除之外T任何机制。即否定类型限制。Action

想像:

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

Answers:


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:对于type的属性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对于任何使用者而言都有些混乱,因此这可能不是最佳解决方案。可能有一些我尚未发现的问题。如果发现任何问题,请告诉我😊


2
我觉得白人刚达夫刚刚露面了。🤯TBH我已经准备好将此作为编译器的限制而注销。所以很高兴尝试一下。谢谢!🙇–
ctrlplusb

@ctrlplusb😂,大声笑,那条评论使我
高兴

我本打算对这个答案应用赏金计划,但我的睡眠严重不足,婴儿的大脑持续不断地被误点击。我很抱歉!这是一个极富洞察力的答案。尽管性质很复杂。so非常感谢您抽出宝贵的时间回答问题。
ctrlplusb

@ctrlplusb :(哦,好吧..赢了一些,输了一些:)
Titian Cernicova-Dragomir

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
}

不理想,但是很
高兴

1

countvalue总是使编译器不满意。要解决此问题,您可以尝试执行以下操作:

{
  value,
  count: 1,
  transform: (data: Partial<Thing<T>>) => {
   ...
  }
}

由于Partial使用的是实用程序类型,因此在transform不存在方法的情况下可以。

Stackblitz


1
“价值和价值将永远使编译器不满意”-我希望对这里的原因有所了解。xx
ctrlplusb,

1

通常,我读过两次,但并不完全了解您想要实现的目标。根据我的理解,您想省略transform确切的类型transform。要做到这一点很简单,我们需要使用Omit

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

由于您在其他实用程序类型中提供了相当复杂的功能,因此不确定是否要这样做。希望能帮助到你。


谢谢,是的,我希望。但这是我为第三方消费而导出的实用程序类型。我不知道他们物体的形状/特性。我只知道我需要剥离所有函数属性,并将结果用于transform func数据参数。
ctrlplusb

我已经更新了我的问题描述,希望可以使它更清晰。
ctrlplusb

2
主要问题是T也可以是Action类型,因为它没有定义为排除它。希望会找到一些解决方案。但是我在可以计数的地方,但是T仍然被省略,因为它与Action相交
Maciej Sikora

如果我能表示T不是Action类型,那将是很好。扩展的倒数。
ctrlplusb

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.