[typescript] 유형 매핑시 일반 속성 관련 문제

다음과 유사한 유틸리티 유형을 내보내는 라이브러리가 있습니다.

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. 즉 네거티브 타입 제한.

상상해보십시오.

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 에서이 코드 예제를 사용할 수 있습니다.



답변

이것은 흥미로운 문제입니다. 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는 모든 소비자에게 약간 혼란 스럽기 때문에 이것이 최선의 해결책이 아닐 수도 있습니다. 아직 확인하지 않은 문제가있을 수 있습니다. 당신이 발견하면 알려주세요 ?


답변

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
}


답변

countvalue 항상 컴파일러가 불행 할 것이다. 이 문제를 해결하려면 다음과 같이 시도하십시오.

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

이후 Partial 유틸리티 종류를 사용하는, 당신이 경우에 확인 될 것입니다transform 방법은 존재하지 않습니다.

스택 블리츠


답변

일반적으로 나는 그것을 두 번 읽고 당신이 달성하고자하는 것을 완전히 이해하지 못합니다. 내 이해에서 당신 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
    },
  };
}

추가 유틸리티 유형에서 제공 한 복잡성으로 인해 이것이 원하는 것인지 확실하지 않습니다. 도움이 되길 바랍니다.


답변