Typescript:根据枚举参数值的条件函数返回类型

huangapple go评论58阅读模式
英文:

Typescript: Conditional function return type based on enum parameter value

问题

以下是代码部分的翻译:

// enumeration of possible partners 
enum Partner {
    Google = 'google',
    Microsoft = 'microsoft'
}

// lets say it's a domain entity, we'll map partner DTOs to it
type Entity = {
    id: string
}

// DTO and mapper function for Partner.Google
type GoogleDto = {
    google_id: string
}

function mapFromGoogle(dto: GoogleDto): Entity {
    return {
        id: dto.google_id
    }
}

// DTO and mapper function for Partner.Microsoft
type MicrosoftDto = {
    ms_id: number
}

function mapFromMicrosoft(dto: MicrosoftDto): Entity {
    return {
        id: String(dto.ms_id)
    }
}

// And here I am trying to use a conditional type
// to check which enum value is passed as an argument
// and to provide a correct return type

function toEntity<T extends Partner>(partner: T): T extends Partner.Google ? typeof mapFromGoogle : typeof mapFromMicrosoft {
    switch(partner) {
        case Partner.Google:
            return mapFromGoogle // Type '(dto: GoogleDto) => Entity' is not assignable to type 'T extends Partner.Google ? (dto: GoogleDto) => Entity : (dto: MicrosoftDto) => Entity'
        case Partner.Microsoft:
            return mapFromMicrosoft // Type '(dto: MicrosoftDto) => Entity' is not assignable to type 'T extends Partner.Google ? (dto: GoogleDto) => Entity : (dto: MicrosoftDto) => Entity'
        default:
        throw new Error('Unsupported partner')
    }
}

const e1 = toEntity(Partner.Google)({ google_id: 'id' }) 
const e2 = toEntity(Partner.Google)({ google_id: 'id', ms_id: 4 }) // 'ms_id' does not exist in type 'GoogleDto'
const e3 = toEntity(Partner.Microsoft)({ ms_id: 10 })
const e4 = toEntity(Partner.Microsoft)({ ms_id: 10, google_id: 'asd' }) // 'google_id' does not exist in type 'MicrosoftDto'
const e5 = toEntity(Partner.Google)({}) // Property 'google_id' is missing in type '{}' but required in type 'GoogleDto'
const e6 = toEntity(Partner.Microsoft)({}) // Property 'ms_id' is missing in type '{}' but required in type 'MicrosoftDto'

如果您需要更多帮助或有其他问题,请随时提出。

英文:

looking for some help in typing a factory function that accepts a single enum as a paramter and returns a mapper function.

// enumeration of possible partners 
enum Partner {
    Google = &#39;google&#39;,
    Microsoft = &#39;microsoft&#39;
}

// lets say it&#39;s a domain entity, we&#39;ll map partner DTOs to it
type Entity = {
    id: string
}

// DTO and mapper function for Partner.Google
type GoogleDto = {
    google_id: string
}

function mapFromGoogle(dto: GoogleDto): Entity {
    return {
        id: dto.google_id
    }
}

// DTO and mapper function for Partner.Microsoft
type MicrosoftDto = {
    ms_id: number
}

function mapFromMicrosoft(dto: MicrosoftDto): Entity {
    return {
        id: String(dto.ms_id)
    }
}

// And here I am trying to use a conditional type
// to check which enum value is passed as an argument
// and to provide a correct return type

function toEntity&lt;T extends Partner&gt;(partner: T): T extends Partner.Google ? typeof mapFromGoogle : typeof mapFromMicrosoft {
    switch(partner) {
        case Partner.Google:
            return mapFromGoogle // Type &#39;(dto: GoogleDto) =&gt; Entity&#39; is not assignable to type &#39;T extends Partner.Google ? (dto: GoogleDto) =&gt; Entity : (dto: MicrosoftDto) =&gt; Entity&#39;
        case Partner.Microsoft:
            return mapFromMicrosoft // Type &#39;(dto: MicrosoftDto) =&gt; Entity&#39; is not assignable to type &#39;T extends Partner.Google ? (dto: GoogleDto) =&gt; Entity : (dto: MicrosoftDto) =&gt; Entity&#39;
        default:
        throw new Error(&#39;Unsupported partner&#39;)
    }
}

const e1 = toEntity(Partner.Google)({ google_id: &#39;id&#39; }) 
const e2 = toEntity(Partner.Google)({ google_id: &#39;id&#39;, ms_id: 4 }) // &#39;ms_id&#39; does not exist in type &#39;GoogleDto&#39;
const e3 = toEntity(Partner.Microsoft)({ ms_id: 10 })
const e4 = toEntity(Partner.Microsoft)({ ms_id: 10, google_id: &#39;asd&#39; }) // &#39;google_id&#39; does not exist in type &#39;MicrosoftDto&#39;
const e5 = toEntity(Partner.Google)({}) // Property &#39;google_id&#39; is missing in type &#39;{}&#39; but required in type &#39;GoogleDto&#39;
const e6 = toEntity(Partner.Microsoft)({}) // Property &#39;ms_id&#39; is missing in type &#39;{}&#39; but required in type &#39;MicrosoftDto&#39;

The resulting function works as expected, it correctly relies on a provided partner and errors if invalid DTO id provided.

But TS shows error when I'm trying to return a specific mapper function from the toEntity function inside the case block.

Asking if someone can point me to the right direction in order to solve this case.

I've tried converting enumeration to union, but it also doesn't work.
Also tried to remove return type relying on TS to infer the type of returned mapper function, but in this case type checking doesn't work when toEntity is called.

答案1

得分: 0

TypeScript类型检查器无法准确推断依赖于尚未指定的泛型类型参数的条件类型可能或不可能被分配的值。它推迟对这些类型的评估。在toEntity()函数体内,类型

T extends Partner.Google ? typeof mapFromGoogle : typeof mapFromMicrosoft

是这样一种泛型条件类型,所以编译器不确定它会是什么……它不知道T确切是什么,所以它对T extends Partner.Google ? typeof mapFromGoogle : typeof mapFromMicrosoft几乎一无所知。

你可能认为,检查partner参数与不同可能性的switch/case语句会帮助缩小/重新约束T,但至少在TypeScript 4.9中,实际情况并非如此。

更好的解决方法是将操作重构为泛型属性查找,而不是泛型switch/case。编译器理解泛型索引访问类型,而不理解泛型条件类型。

以下是一种编写方法:

const partnerMap = {
  [Partner.Google]: mapFromGoogle,
  [Partner.Microsoft]: mapFromMicrosoft
}
type PartnerMap = typeof partnerMap;
function toEntity<K extends Partner>(partner: K): PartnerMap[K] {
  return partnerMap[partner];
}

partnerMap对象编码了toEntity()所需的输入-输出关系,以键值对的形式。toEntity的调用签名表示返回类型与输入类型之间通过在partnerMap类型中查找相关联。实现已进行更改以匹配这种类型。这个版本可以编译,因为类型检查器同意在PartnerMap类型的值中查找类型为K的键将产生类型为PartnerMap[K]的值。

并且让我们确保它从调用方的角度起作用:

const g = toEntity(Partner.Google); 
// const g: (dto: GoogleDto) => Entity
const m = toEntity(Partner.Microsoft);
// const m: (dto: MicrosoftDto) => Entity

看起来很好;这部分与你的版本没有变化,所以所有的测试用例都表现一致。

英文:

The TypeScript type checker is not really able to reason about what values might or might not be assignable to a conditional type that depends on an as-yet unspecified generic type parameter. It defers evaluation of such types. Inside the body of toEntity(), the type

T extends Partner.Google ? typeof mapFromGoogle : typeof mapFromMicrosoft

is such a generic conditional type, and so the compiler is not sure what it's going to be... it doesn't know exactly what T is, so it knows almost nothing about T extends Partner.Google ? typeof mapFromGoogle : typeof mapFromMicrosoft.

You might think that your switch/case statements that check the partner parameter against the different possibilities would help narrow/re-constrain T, but this just doesn't happen, at least as of TypeScript 4.9.

The canonical feature request for something better is microsoft/TypeScript#33912. It's been open for quite a while and there's no indication when it might be implemented, if ever.

For now, if you want to proceed, you'll need to work around it.


The best workaround in my opinion is to refactor your operations so that they are represented as a generic property lookup instead of a generic switch/case. The compiler understands generic indexed access types in a way it does not understand generic conditional types.

Here's one way to write that:

const partnerMap = {
[Partner.Google]: mapFromGoogle,
[Partner.Microsoft]: mapFromMicrosoft
}
type PartnerMap = typeof partnerMap;
function toEntity&lt;K extends Partner&gt;(partner: K): PartnerMap[K] {
return partnerMap[partner];
}

The partnerMap object encodes the desired input-output relationship of toEntity() as key-value pairs. The toEntity call signature says that the return type is related to the input type via a lookup into the type of partnerMap. And the implementation has been changed to match. This compiles because the type checker agrees that looking up a key of type K in a value of type PartnerMap will yield a value of type PartnerMap[K].


And let's just make sure that it works as desired from the caller's side:

const g = toEntity(Partner.Google); 
// const g: (dto: GoogleDto) =&gt; Entity
const m = toEntity(Partner.Microsoft);
// const m: (dto: MicrosoftDto) =&gt; Entity

Looks good; this part hasn't changed from your version, so all the test cases behave the same.

Playground link to code

huangapple
  • 本文由 发表于 2023年2月9日 02:06:17
  • 转载请务必保留本文链接:https://go.coder-hub.com/75389996.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定