Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
364 views
in Technique[技术] by (71.8m points)

javascript - How to make Discriminated Unions work with destructuring?

Consider the following three types in which MainType is union of Type1 and Type2. If kind is "kind1" then data should be of type {msg: string} same from Type2

interface Type1 {
    kind: "kind1";
    data: { msg: string };
}
interface Type2 {
    kind: "kind2";
    data: { msg2: string };
}

type MainType = Type1 | Type2;

Here is the first way to use it.

function func(obj: MainType) {
    switch (obj.kind) {
        case "kind1": return obj.data.msg;
        case "kind2": return obj.data.msg2;
    }
}

The above code gives no error and shows correct autocomplete.

But when we destructure the obj then it gives error.

function func({kind, data}: MainType) {
    switch (kind) {
        case "kind1": return data.msg;
        case "kind2": return data.msg2;
    }
}

The error is

Property 'msg' does not exist on type '{ msg: string; } | { msg2: string; }'

Maybe its something very basic. But I am new to ts so I can't get how destructuring changes the types. Please explain the reason and also tell is there any way to fix it.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

By using destructuring at level of function argument we loose connection between kind and data. So switch by kind is not narrowing the data as now they are in different data structures.

I can say you remove the bound between kind and data what means that you really introduce two variables, one with type kind1 | kind2 and second with type { msg: string; } | { msg2: string; }.

In result we don't have discriminant in form of kind anymore.

Below equivalent code to destructuring behavior:

const f = (t: MainType) => {
  const kind = t.kind // "kind1" | "kind2";
  const data = t.data // {msg: string;} | {msg2: string;}
}

And yes from the logic perspective your code is fully ok, it should work as we know the relation between these fields. Unfortunately TS is not able to understand the bound.

In summary - unfortunate until your don't narrow the type to specific member of the union, you cannot use destructuring, as it will ruin the type relationship between fields.


We can think about workaround by some type guards. Consider following example:

const isKind1 = (kind: MainType['kind'], data: MainType['data']): data is Type1['data'] 
=> kind === 'kind1'
const isKind2 = (kind: MainType['kind'], data: MainType['data']): data is Type2['data'] 
=> kind === 'kind2'

const f = ({kind, data}: MainType) => {
  if (isKind1(kind, data)) {
    data // is { msg: string }
  }
  if (isKind2(kind, data)) {
    data // is { msg2: string }
  }
}

By using type guards isKind1 and isKind2 we are able to create a connection between these two variables. But the issue is we cannot use switch anymore, we also have more code, and field relation implemented in functions and not type definitions, such approach is error prone as I can do different relation in function then the original type is defining.

To be clear I am showing it is possible but its not worth the candle and I suggest to keep the original implementation without destructuring.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...