2017-09-20 1 views
10

단일 인수를 문자열로 받아들이고 일치하는 type 속성을 가진 객체를 반환하는 클래스 메서드가 있습니다. 이 메서드는 식별 된 공용체 형식을 축소하는 데 사용되며 반환 된 개체가 항상 type 구분 값을 갖는 특정 좁혀진 형식이어야합니다.TypeScript의 일반, 식별 된 공용체에서 반환 유형 좁히기

제네릭 매개 변수에서 형식을 정확하게 좁히는이 메서드에 대한 형식 시그니처를 제공하려하지만 사용자가 명시 적으로 범위를 좁혀야하는 형식화 된 유니온에서이를 구별하려고 시도하는 것은 아무것도 아닙니다. 아래로. 그건 효과가 있지만 성가시다.

는 희망이 최소 재생은 명확하게 :

interface Action { 
    type: string; 
} 

interface ExampleAction extends Action { 
    type: 'Example'; 
    example: true; 
} 

interface AnotherAction extends Action { 
    type: 'Another'; 
    another: true; 
} 

type MyActions = ExampleAction | AnotherAction; 

declare class Example<T extends Action> { 
    // THIS IS THE METHOD IN QUESTION 
    doSomething<R extends T>(key: R['type']): R; 
} 

const items = new Example<MyActions>(); 

// result is guaranteed to be an ExampleAction 
// but it is not inferred as such 
const result1 = items.doSomething('Example'); 

// ts: Property 'example' does not exist on type 'AnotherAction' 
console.log(result1.example); 

/** 
* If the dev provides the type more explicitly it narrows it 
* but I'm hoping it can be inferred instead 
*/ 

// this works, but is not ideal 
const result2 = items.doSomething<ExampleAction>('Example'); 
// this also works, but is not ideal 
const result3: ExampleAction = items.doSomething('Example'); 

가 나는 또한 동적으로 "매핑 유형"구축을 시도, 영리 점점 시도 - TS에서 상당히 새로운 기능입니다.

declare class Example2<T extends Action> { 
    doSomething<R extends T['type'], TypeMap extends { [K in T['type']]: T }>(key: R): TypeMap[R]; 
} 

이 같은 결과를 앓고 : 그것은 때문에 형태 맵 { [K in T['type']]: T } 각 계산 된 재산, T에 대한 값의 유형을 축소하지 않습니다는 K in 반복의 각 속성에 대해하지 입니다 대신하다 똑같은 MyActions 노동 조합. 사용자가 사용할 수있는 미리 정의 된 매핑 된 유형을 제공하도록 요구하는 경우 실제로 작동하지만 매우 실용적인 개발자 경험이 될 수 있으므로 옵션이 아닙니다. (노동 조합은 거대합니다)


이 사례는 이상하게 보일 수 있습니다. 내 문제를 더 소모적 인 형태로 추출하려했지만 실제로 사용 사례는 Observables에 관한 것입니다. 익숙하다면 ofType operator provided by redux-observable을보다 정확하게 입력하려고합니다. 기본적으로 filter() on the type property의 약자입니다.

이것은 사실 Observable#filterArray#filter도 유형을 좁히는 것과 매우 유사하지만, 술어 콜백에 value is S 반환 값이 있기 때문에 TS가이를 파악한 것으로 보입니다. 여기서 내가 비슷한 것을 어떻게 적용 할 수 있는지는 명확하지 않다.

답변

10

프로그램에서 많은 좋은 솔루션 등을 당신은 간접 레이어를 추가하여 얻을 수 있습니다.

특히 여기서 수행 할 수있는 것은 액션 태그 사이에 표를 추가하는 것입니다."Example""Another") 및 각각의 페이로드.

type TagWithKey<TagName extends string, T> = { 
    [K in keyof T]: { [_ in TagName]: K } & T[K] 
}; 

우리는 사이에 테이블을 생성하는 데 사용할 :

type ActionPayloadTable = { 
    "Example": { example: true }, 
    "Another": { another: true }, 
} 

는 우리가 할 수있는 일은 각 작업 태그에 매핑되는 특정 속성을 각 페이로드를 태그 도우미 유형을 만드는 것입니다 행동 유형 및 전체 동작은 스스로 객체 :

:이 글을 쓰는 쉬운 (이기는하지만 방법이 명확하지) 방식이었다
type ActionTable = TagWithKey<"type", ActionPayloadTable>; 

type MyActions = ExampleAction | AnotherAction; 

를 작성하여

type ExampleAction = ActionTable["Example"]; 
type AnotherAction = ActionTable["Another"]; 

그리고 우리는 노동 조합을 만들 수 있습니다 또는 우리는 노동 조합 각 업데이트에서 자신을 마련 할 수있는 :

type ActionTable = { 
    "Example": { type: "Example" } & { example: true }, 
    "Another": { type: "Another" } & { another: true }, 
} 

이제 우리는 밖으로 행동의 각 편리 이름을 만들 수 있습니다 쓰기로 새로운 작업을 추가 할 때

type Unionize<T> = T[keyof T]; 

type MyActions = Unionize<ActionTable>; 

마지막으로 우리는 네가 가진 계급. 액션에 매개 변수를 지정하는 대신 액션 테이블에서 매개 변수화합니다. 아마도 가장 의미 할 것이다 부분

declare class Example<Table> { 
    doSomething<ActionName extends keyof Table>(key: ActionName): Table[ActionName]; 
} 

-Example는 기본적으로 단지의 출력에 테이블의 입력을 매핑합니다.

전체 코드는 다음과 같습니다.

/** 
* Adds a property of a certain name and maps it to each property's key. 
* For example, 
* 
* ``` 
* type ActionPayloadTable = { 
*  "Hello": { foo: true }, 
*  "World": { bar: true }, 
* } 
* 
* type Foo = TagWithKey<"greeting", ActionPayloadTable>; 
* ``` 
* 
* is more or less equivalent to 
* 
* ``` 
* type Foo = { 
*  "Hello": { greeting: "Hello", foo: true }, 
*  "World": { greeting: "World", bar: true }, 
* } 
* ``` 
*/ 
type TagWithKey<TagName extends string, T> = { 
    [K in keyof T]: { [_ in TagName]: K } & T[K] 
}; 

type Unionize<T> = T[keyof T]; 

type ActionPayloadTable = { 
    "Example": { example: true }, 
    "Another": { another: true }, 
} 

type ActionTable = TagWithKey<"type", ActionPayloadTable>; 

type ExampleAction = ActionTable["Example"]; 
type AnotherAction = ActionTable["Another"]; 

type MyActions = Unionize<ActionTable> 

declare class Example<Table> { 
    doSomething<ActionName extends keyof Table>(key: ActionName): Table[ActionName]; 
} 

const items = new Example<ActionTable>(); 

const result1 = items.doSomething("Example"); 

console.log(result1.example); 
+2

후손을 위해 내가 원하는 것은 실제로 TS에 의해 현재 지원되지 않습니다. https://github.com/Microsoft/TypeScript/issues/17915에 요청하는 기존 티켓이 있습니다. 실제로 유연하게 사용할 수 있는지는 잘 모르겠지만 가장 유연한 절충안을 제공합니다. – jayphelps

+2

도 똑똑했다 af – jayphelps

+1

'Unionize '이'T [keyof T]'가 아닌 이유는 무엇입니까? – jcalz

1

불행히도 공용체 유형 (예 : type MyActions = ExampleAction | AnotherAction;)을 사용하여이 동작을 수행 할 수 없습니다. If we have a value that has a union type, we can only access members that are common to all types in the union.

그러나 솔루션은 중대하다. 필요로하는 유형을 정의 할 때이 방법을 사용해야합니다.

const result2 = items.doSomething<ExampleAction>('Example'); 

당신이 좋아하지는 않지만, 원하는대로 할 수있는 합법적 인 방법입니다.

+0

고마워요! 나는 당신이 좁히는 차별적 인 노조가있을 때 견적이 적용되지 않는다고 믿습니다. https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions 그러나 내 문제는 TS가 현재 좁혀 진 유형을 추론하지 않고 대신 명시 적으로 제공해야한다는 것입니다 (언급 한대로). 이것이 가능해야만하는 의심스러운 이유는 TS가 올바른 형식을 일반 매개 변수로만 받아들이는 것처럼 보이는 것입니다. 즉, 'ExampleAction'의 모양 이외의 다른 형식으로 전달하면 TS가이를 거부합니다. – jayphelps

+0

예 : 'items.doSomething ('Example')'이 올바르게 생성됩니다 :' 'Example'유형의 인수를 'Another'유형의 매개 변수에 지정할 수 없습니다. 가장 좋은 추측은 그렇게 할 유형 정보가 있더라도 추측 된 범위 지정을 현재 지원하지 않는다는 것입니다. – jayphelps

+0

그래, 차별화 된 노동 조합을 얻는 유일한 방법은 지금 코드의 사례를 다루는 것 같아. 아마 그들은 언젠가 당신이 원하는 기능을 지원하게 될 것입니다. 나는 더 명백한 방법으로 고집 할 것이다. –

1

조금 더 자세한 설정에 그러나 우리는 유형 조회하여 원하는 API 얻을 수 있습니다

+0

나는 이것이 내가 언급 한 것과 동일하다고 생각한다. "사용자가 사용할 수있는 사전 정의 된 매핑 된 유형을 제공하도록 요구하면 실제로 작동하지만 매우 실용적인 개발자 경험이 될 수있는 옵션이 아닙니다. 노동 조합은 거대하다) "아니오? – jayphelps

+0

조금 더 명확히하기 위해, dev는 이미 모든 액션의 조합을 제공해야하므로 추가 유형 맵뿐만 아니라이를 제공해야합니다. 지도의 값에서 노동 조합을 만드는 방법이 없다면? 그게 불가능하다고 가정하면, 이것은 devs를 삼키기 힘든 요구 사항이 될 것입니다 - 그들은 명시적인 일반 패스'items.doSomething ('Example')' – jayphelps

+0

을 선호한다고 생각합니다. 경고)를 사용하여 형식 맵에서 유니온을 가져옵니다. Daniel의 미친 대답 lololol – jayphelps

2

이렇게하면 a change in TypeScript이 질문과 똑같이 작동해야합니다.

클래스가 단일 객체의 속성으로 그룹화 될 수 있으면 수용된 응답도 도움이 될 수 있습니다. 거기에 Unionize<T> 트릭을 좋아합니다.

실제 문제를 설명하기 위해, 내가이에 예를 좁혀 보자 : 당신이 할 수

class RedShape { 
    color: 'Red' 
} 

class BlueShape { 
    color: 'Blue' 
} 

type Shapes = RedShape | BlueShape; 

type AmIRed = Shapes & { color: 'Red' }; 
/* Equals to 

type AmIRed = (RedShape & { 
    color: "Red"; 
}) | (BlueShape & { 
    color: "Red"; 
}) 
*/ 

/* Notice the last part in before: 
(BlueShape & { 
    color: "Red"; 
}) 
*/ 
// Let's investigate: 
type Whaaat = (BlueShape & { 
    color: "Red"; 
}); 
type WhaaatColor = Whaaat['color']; 

/* Same as: 
    type WhaaatColor = "Blue" & "Red" 
*/ 

// And this is the problem. 

또 다른 점은 함수에 실제 클래스를 전달합니다. 여기에 미친 예는 다음과 같습니다

declare function filterShape< 
    TShapes, 
    TShape extends Partial<TShapes> 
    >(shapes: TShapes[], cl: new (...any) => TShape): TShape; 

// Doesn't run because the function is not implemented, but helps confirm the type 
const amIRed = filterShape(new Array<Shapes>(), RedShape); 
type isItRed = typeof amIRed; 
/* Same as: 
type isItRed = RedShape 
*/ 

여기서 문제는 color의 값을 얻을 수있다. RedShape.prototype.color을 사용할 수 있지만이 값은 생성자에만 적용되므로 항상 정의되지 않습니다.그래서,

var RedShape = /** @class */ (function() { 
    function RedShape() { 
     this.color = 'Red'; 
    } 
    return RedShape; 
}()); 

그리고 실제 예를 들어, 생성자 등 여러 매개 변수를 가질 수 있습니다 RedShape는 컴파일되어

var RedShape = /** @class */ (function() { 
    function RedShape() { 
    } 
    return RedShape; 
}()); 

그리고 당신이 할 경우에도 :로 컴파일

class RedShape { 
    color: 'Red' = 'Red'; 
} 

은 인스턴스화가 불가능할 수도 있습니다. 인터페이스에 대해서도 작동하지는 않습니다.

당신은 바보 같은 방법으로 복귀 할 수는 :

class Action1 { type: '1' } 
class Action2 { type: '2' } 
type Actions = Action1 | Action2; 

declare function ofType<TActions extends { type: string }, 
    TAction extends TActions>(
    actions: TActions[], 
    action: new(...any) => TAction, type: TAction['type']): TAction; 

const one = ofType(new Array<Actions>(), Action1, '1'); 
/* Same as if 
var one: Action1 = ... 
*/ 

아니면 doSomething 표현의

는 :

다른 대답에 단 댓글에서 언급 한 바와 같이
declare function doSomething<TAction extends { type: string }>(
    action: new(...any) => TAction, type: TAction['type']): TAction; 

const one = doSomething(Action1, '1'); 
/* Same as if 
const one : Action1 = ... 
*/ 

의에 문제가 있습니다 이미 추론 문제를 해결하기위한 TypeScript. 나는이 대답의 설명으로 연결되는 주석을 썼고, 문제의 상위 레벨 예제를 제공했다. here.

+1

대단한 설명! +1 – jayphelps