ปัญหาเกี่ยวกับคุณสมบัติทั่วไปเมื่อการแมปประเภท


11

ฉันมีห้องสมุดที่ส่งออกประเภทยูทิลิตี้คล้ายกับต่อไปนี้

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สำหรับ เช่นข้อ จำกัด ประเภทเชิงลบ

Imagine:

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

คำตอบ:


7

นี่คือปัญหาที่น่าสนใจ. typescript โดยทั่วไปไม่สามารถทำอะไรได้มากมายเกี่ยวกับพารามิเตอร์ประเภททั่วไปในประเภทเงื่อนไข เป็นเพียงการประเมินใด ๆextendsหากพบว่าการประเมินนั้นเกี่ยวข้องกับพารามิเตอร์ประเภท

ข้อยกเว้นใช้ถ้าเราสามารถรับ typescript เพื่อใช้ความสัมพันธ์ชนิดพิเศษคือความสัมพันธ์ความเท่าเทียมกัน (ไม่ขยายความสัมพันธ์) ความสัมพันธ์ที่เท่าเทียมกันนั้นง่ายต่อการเข้าใจสำหรับคอมไพเลอร์ดังนั้นจึงไม่จำเป็นต้องเลื่อนการประเมินประเภทตามเงื่อนไข ข้อ จำกัด ทั่วไปเป็นหนึ่งในไม่กี่แห่งในคอมไพเลอร์ที่ใช้ความเท่าเทียมกันของประเภท ลองดูตัวอย่าง:

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 นั้นค่อนข้างสับสนสำหรับผู้บริโภคดังนั้นนี่อาจไม่ใช่ทางออกที่ดีที่สุด อาจมีปัญหาที่ฉันยังไม่ได้ระบุ หากคุณพบใด ๆ แจ้งให้เราทราบ😊


2
ฉันรู้สึกเหมือน Gandalf the White เพิ่งเปิดเผยตัวเอง BH TBH ฉันพร้อมที่จะเขียนเรื่องนี้เป็นข้อ จำกัด ของคอมไพเลอร์ ลองเอาไปลองดู ขอบคุณ! 🙇
ctrlplusb

@ctrlplusb 😂ฮ่า ๆ ความคิดเห็นนั้นทำให้วันของฉัน😊
Titian Cernicova-Dragomir

ฉันตั้งใจจะนำเงินจำนวนนี้ไปใช้กับคำตอบนี้ แต่ฉันยังขาดสมองเด็กน้อยที่หลับไม่สนิทและเข้าใจผิด ขอโทษด้วย! นี่คือคำตอบที่ลึกซึ้งอย่างน่าอัศจรรย์ แม้ว่าจะค่อนข้างซับซ้อนในธรรมชาติ 😅ขอบคุณมากที่สละเวลาตอบคำถาม
ctrlplusb

@ctrlplusb :( โอ้ดี .. ชนะบางสูญเสีย :) บาง
ทิเชียน Cernicova-Dragomir

2

คงจะดีมากถ้าฉันสามารถบอกได้ว่า T ไม่ใช่ประเภทแอคชั่น การเรียงลำดับของค่าผกผันของส่วนขยาย

เหมือนกับที่คุณพูดปัญหาคือเรายังไม่มีข้อ จำกัด เชิงลบ ฉันหวังว่าพวกเขาจะได้รับคุณสมบัติเช่นนี้ในไม่ช้า ระหว่างรอฉันเสนอวิธีแก้ปัญหาแบบนี้:

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
}

ไม่เหมาะ แต่ดีที่รู้วิธีแก้ปัญหาแบบกึ่ง :)
ctrlplusb

1

countและvalueจะทำให้คอมไพเลอร์ไม่มีความสุขเสมอ หากต้องการแก้ไขคุณอาจลองทำสิ่งนี้:

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

ไม่แน่ใจว่านี่เป็นสิ่งที่คุณต้องการหรือไม่เนื่องจากความซับซ้อนที่คุณให้ในยูทิลิตี้ประเภทเพิ่มเติม หวังว่ามันจะช่วย


ขอบคุณใช่ฉันต้องการ แต่นี่คือยูทิลิตี้ประเภทที่ฉันกำลังส่งออกเพื่อการบริโภคของบุคคลที่สาม ฉันไม่รู้รูปร่าง / คุณสมบัติของวัตถุ ฉันเพิ่งรู้ว่าฉันจำเป็นต้องตัดคุณสมบัติของฟังก์ชั่นทั้งหมดและใช้ผลกับการแปลงข้อมูลอาร์กิวเมนต์ func
ctrlplusb

ฉันได้อัปเดตคำอธิบายปัญหาของฉันโดยหวังว่าจะทำให้ชัดเจนยิ่งขึ้น
ctrlplusb

2
ปัญหาหลักคือ T สามารถเป็นประเภทการกระทำได้เนื่องจากไม่ได้กำหนดให้ยกเว้น หวังว่าจะได้พบกับการแก้ปัญหาบางอย่าง แต่ฉันอยู่ในสถานที่ที่นับเป็น ok แต่ T ก็ยังถูกละไว้เพราะมันตัดกับ Action
Maciej Sikora

คงจะดีมากถ้าฉันสามารถบอกได้ว่า T ไม่ใช่ประเภทแอคชั่น การเรียงลำดับของค่าผกผันของส่วนขยาย
ctrlplusb

การอภิปรายเชิงสัมพันธ์: stackoverflow.com/questions/39328700/…
ctrlplusb
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.