다음과 유사한 유틸리티 유형을 내보내는 라이브러리가 있습니다.
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
}
답변
count
과 value
항상 컴파일러가 불행 할 것이다. 이 문제를 해결하려면 다음과 같이 시도하십시오.
{
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
},
};
}
추가 유틸리티 유형에서 제공 한 복잡성으로 인해 이것이 원하는 것인지 확실하지 않습니다. 도움이 되길 바랍니다.