Vấn đề với thuộc tính chung khi ánh xạ kiểu


11

Tôi có một thư viện xuất một loại tiện ích tương tự như sau:

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

Loại tiện ích này cho phép bạn khai báo một chức năng sẽ thực hiện dưới dạng "hành động". Nó nhận được một đối số chung là Modelhành động sẽ chống lại.

Đối datasố của "hành động" sau đó được gõ với một loại tiện ích khác mà tôi xuất;

type State<Model extends object> = Omit<Model, KeysOfType<Model, Action<any>>>;

Các Stateloại tiện ích cơ bản mất đến Modelchung chung và sau đó tạo ra một loại mới, nơi tất cả các tính năng mà các loại Actionđã được gỡ bỏ.

Ví dụ ở đây là một người sử dụng đất cơ bản thực hiện các điều trên;

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

Ở trên đang làm việc rất tốt. 👍

Tuy nhiên, có một trường hợp mà tôi đang vật lộn, cụ thể là khi định nghĩa mô hình chung được xác định, cùng với hàm xuất xưởng để tạo ra các thể hiện của mô hình chung.

Ví dụ;

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

Trong ví dụ trên, tôi hy vọng datađối số sẽ được nhập vào nơi doSomethinghành động đã bị xóa và thuộc tính chung valuevẫn tồn tại. Tuy nhiên đây không phải là trường hợp - valuetài sản cũng đã bị xóa bởi Statetiện ích của chúng tôi .

Tôi tin rằng nguyên nhân cho điều này là Tchung chung mà không có bất kỳ hạn chế / thu hẹp nào được áp dụng cho nó, và do đó hệ thống loại quyết định rằng nó giao với một Actionloại và sau đó loại bỏ nó khỏi dataloại đối số.

Có cách nào để khắc phục hạn chế này không? Tôi đã làm một số nghiên cứu và đã hy vọng sẽ có một số cơ chế, trong đó tôi có thể nói rằng Tbất kỳ ngoại trừ cho một Action. tức là hạn chế loại âm.

Hãy tưởng tượng:

function modelFactory<T extends any except Action<any>>(value: T): UserDefinedModel<T> {

Nhưng tính năng đó không tồn tại cho TypeScript.

Có ai biết một cách tôi có thể làm cho nó hoạt động như tôi mong đợi không?


Để hỗ trợ gỡ lỗi ở đây là một đoạn mã đầy đủ:

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

Bạn có thể chơi với ví dụ mã này tại đây: https://codesandbox.io/s/reverent-star-m4sdb?fontsize=14

Câu trả lời:


7

Đây là một vấn đề thú vị. Bản thảo thường không thể làm gì nhiều với các tham số loại chung trong các loại có điều kiện. Nó chỉ trì hoãn mọi đánh giá extendsnếu thấy rằng việc đánh giá liên quan đến một tham số loại.

Một ngoại lệ được áp dụng nếu chúng ta có thể lấy bản thảo để sử dụng một loại quan hệ kiểu đặc biệt, cụ thể là, quan hệ đẳng thức (không phải là quan hệ mở rộng). Một mối quan hệ bình đẳng là đơn giản để hiểu cho trình biên dịch, do đó không cần phải trì hoãn việc đánh giá loại điều kiện. Các ràng buộc chung là một trong số ít các vị trí trong trình biên dịch nơi sử dụng đẳng thức. Hãy xem xét một ví dụ:

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

Liên kết sân chơi

Chúng ta có thể lợi dụng hành vi này để xác định các loại cụ thể. Bây giờ, đây sẽ là một loại khớp chính xác, không phải là một trận đấu mở rộng và các loại khớp chính xác không phải lúc nào cũng phù hợp. Tuy nhiên, kể từAction chỉ là một chữ ký hàm, nên các loại khớp chính xác có thể hoạt động đủ tốt.

Hãy xem liệu chúng ta có thể trích xuất các loại khớp với chữ ký hàm đơn giản hơn không, chẳng hạn như (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;
  // }

}

Liên kết sân chơi

Loại trên KeysOfIdenticalTypegần với những gì chúng ta cần để lọc. Đối với other, tên tài sản được bảo tồn. Đối với action, tên tài sản bị xóa. Chỉ có một vấn đề đáng lo ngại xung quanh value. Vì valuelà loại T, không thể giải quyết một cách tầm thường T, và (v: T) => voidkhông giống nhau (và trên thực tế chúng có thể không).

Chúng ta vẫn có thể xác định đó valuelà giống hệt với T: đối với các thuộc tính của loại T, giao cắt kiểm tra này (v: T) => voidvới never. Bất kỳ giao lộ với neverlà giải quyết tầm thường never. Sau đó chúng ta có thể thêm các thuộc tính trở lại của loại Tbằng cách sử dụng kiểm tra danh tính khác:

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

}

Liên kết sân chơi

Giải pháp cuối cùng trông giống như thế này:

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

Liên kết sân chơi

GHI CHÚ: Hạn chế ở đây là điều này chỉ hoạt động với một tham số loại (mặc dù nó có thể có thể được điều chỉnh phù hợp hơn). Ngoài ra, API hơi khó hiểu đối với bất kỳ người tiêu dùng nào, vì vậy đây có thể không phải là giải pháp tốt nhất. Có thể có những vấn đề mà tôi chưa xác định được. Nếu bạn tìm thấy bất kỳ, cho tôi biết


2
Tôi cảm thấy như Gandalf the White vừa tiết lộ chính mình. TBH Tôi đã sẵn sàng viết nó ra như một giới hạn của trình biên dịch. Vì vậy, phấn khích để thử này. Cảm ơn bạn! 🙇
ctrlplusb

@ctrlplusb LOL, nhận xét đó đã tạo nên ngày của tôi
Titian Cernicova-Dragomir

Tôi có nghĩa là áp dụng tiền thưởng cho câu trả lời này nhưng tôi bị thiếu ngủ trầm trọng đang diễn ra và hiểu sai. Lời xin lỗi của tôi! Đây là một câu trả lời sâu sắc tuyệt vời. Mặc dù khá phức tạp trong tự nhiên. Cảm ơn bạn rất nhiều vì đã dành thời gian để trả lời nó.
ctrlplusb

@ctrlplusb :( Oh well .. thắng một số thua :)
Titian Cernicova-Dragomir

2

Sẽ thật tuyệt nếu tôi có thể bày tỏ rằng T không thuộc loại Hành động. Sắp xếp một nghịch đảo của phần mở rộng

Chính xác như bạn đã nói, vấn đề là chúng ta chưa có ràng buộc tiêu cực nào. Tôi cũng hy vọng họ có thể hạ cánh tính năng như vậy sớm. Trong khi chờ đợi, tôi đề xuất một cách giải quyết như thế này:

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
}

Không lý tưởng, nhưng thật tuyệt khi biết về một cách giải quyết khác :)
ctrlplusb

1

countvaluesẽ luôn làm cho trình biên dịch không hài lòng. Để khắc phục, bạn có thể thử một cái gì đó như thế này:

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

Partialloại tiện ích đang được sử dụng, nên bạn sẽ ổn thôitransform không có phương pháp.

Stackblitz


1
"Số lượng và giá trị sẽ luôn làm cho trình biên dịch không hài lòng" - Tôi sẽ đánh giá cao một số hiểu biết về lý do tại sao ở đây. xx
ctrlplusb

1

Nói chung tôi đọc nó hai lần và không hiểu đầy đủ những gì bạn muốn đạt được. Theo hiểu biết của tôi, bạn muốn bỏ qua transformloại được đưa ra chính xác transform. Để đạt được điều đó thật đơn giản, chúng ta cần sử dụng 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
    },
  };
}

Không chắc chắn nếu đây là những gì bạn muốn vì khá phức tạp bạn đã đưa ra trong các loại tiện ích bổ sung. Hy vọng nó giúp.


Cảm ơn, vâng tôi ước. Nhưng đây là loại tiện ích tôi đang xuất cho tiêu dùng của bên thứ 3. Tôi không biết hình dạng / tính chất của các đối tượng của họ. Tôi chỉ biết rằng tôi cần phải loại bỏ tất cả các thuộc tính hàm và sử dụng kết quả này để chống lại đối số dữ liệu func biến đổi.
ctrlplusb

Tôi đã cập nhật mô tả vấn đề của mình với hy vọng nó làm cho nó rõ ràng hơn.
ctrlplusb

2
Vấn đề chính là T cũng có thể là loại Hành động vì nó không được xác định để loại trừ nó. Hy vọng sẽ tìm thấy một số giải pháp. Nhưng tôi đang ở nơi đếm được nhưng T vẫn bị bỏ qua vì nó giao với Hành động
Maciej Sikora

Sẽ thật tuyệt nếu tôi có thể bày tỏ rằng T không thuộc loại Hành động. Sắp xếp một nghịch đảo của phần mở rộng.
ctrlplusb

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.