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


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

상상해보십시오.

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유형 매개 변수가 포함 된 것으로 판단되면 평가를 연기합니다 .

특수한 유형 관계, 즉 평등 관계 (확장 관계가 아님) 를 사용하기 위해 유형 스크립트를 얻을 수있는 경우 예외가 적용됩니다 . 평등 관계는 컴파일러에 대해 이해하기 간단하므로 조건부 유형 평가를 연기 할 필요가 없습니다. 일반 제약 조건은 형식 평등이 사용되는 컴파일러의 몇 안되는 장소 중 하나입니다. 예를 보자.

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
난 간달프가 백인을 드러낸 것 같아 TBH 나는 이것을 컴파일러 제한으로 쓸 준비가되었습니다. 그래서 시도해 보았습니다. 감사합니다! 🙇
ctrlplusb

@ctrlplusb 😂 LOL, 그 의견은 내 하루 😊
Titian Cernicova-Dragomir

나는이 답변에 현상금을 적용하려고했지만 수면 아기 뇌가 심각하게 잘못 진행되어 잘못 클릭되었습니다. 사과드립니다! 이것은 환상적인 통찰력있는 답변입니다. 본질적으로 상당히 복잡합니다. 😅 시간을내어 답변 해 주셔서 감사합니다.
ctrlplusb 2018

@ctrlplusb :( 아 글쎄 .. 승리 약간 약간 :) 잃을
티티 Cernicova - 드라고 미르

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
}

이상적이지는 않지만 세미 해결 방법을 아는 것이 좋습니다. :)
ctrlplusb

1

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

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

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

스택 블리츠


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

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


고마워, 그래 그러나 이것은 타사 소비를 위해 내보내는 유틸리티 유형입니다. 나는 그들의 객체의 모양 / 속성을 모른다. 나는 모든 함수 속성을 제거하고 변환 함수 데이터 인수에 대해 결과를 사용해야한다는 것을 알고 있습니다.
ctrlplusb 2018

더 명확하게하기 위해 문제 설명을 업데이트했습니다.
ctrlplusb

2
주요 문제는 T가 제외되도록 정의되지 않았기 때문에 T가 조치 유형일 수도 있다는 것입니다. 희망은 해결책을 찾을 것입니다. 그러나 나는 카운트가 괜찮은 곳에 있지만 액션과 교차하기 때문에 T는 여전히 생략됩니다.
Maciej Sikora

T가 Action 유형이 아니라고 표현할 수 있다면 좋을 것입니다. 확장 된 역의 종류.
ctrlplusb 2018

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.