#How do I tell TS without explicitly typing the return type of a function it's an array of A and/or B

35 messages ยท Page 1 of 1 (latest)

vast brookBOT
#
hornta#4659

Preview:```ts
interface A {}

interface B extends A {}

export const myFunc = () => {
return [...([] as A[]), ...([] as B[])]
}```

normal niche
#

A and B are identical there (both refer to the same type: {}), so A[] is the same as (A | B)[], which is what the return type is inferred as if you drop the annotation

vast brookBOT
#
interface A {}
interface B extends A {}

const myFunc = () => {
  return [
    ...([] as A[]),
    ...([] as B[])
  ]
};

const x = myFunc()
//    ^? - const x: A[]
normal niche
#

and even if they weren't literally identical, since B extends A the type (A | B)[] is still the same type as A[]

vast brookBOT
#
interface A {
  a: 'a'
}
interface B extends A {
  b: 'b'
}

const myFunc = () => {
  return [
    ...([] as A[]),
    ...([] as B[])
  ]
};

const x = myFunc()
//    ^? - const x: A[]
normal niche
#

if they're instead unrelated types, then the union type you expect to be inferred is inferred:

vast brookBOT
#
interface A {
  a: 'a'
}
interface B {
  b: 'b'
}

const myFunc = () => {
  return [
    ...([] as A[]),
    ...([] as B[])
  ]
};

const x = myFunc()
//    ^? - const x: (A | B)[]
modern magnet
#

yes both are an a, but I want to know if this function myFunc CAN return b, now I will believe this function only returns a

#

and TS should be able to know this by parsing the code but I guess TS isn't just there yet? ๐Ÿ˜ฎ

#

then I will use explic types for now.

normal niche
#

the type (A | B)[] is the same as the type A[] in your original example. it's also the same type as {}[]. they're all just different ways to write the same exact thing, and typescript is just picking one to use for display

modern magnet
#

I think it should pick both because I rely on my IDE hovering over types to know those things ๐Ÿ˜ฆ

normal niche
#

a function that returns A can always return a B. in fact there are an infinite number of possible subtypes of A. you should expect A to always mean "exactly A or any subtype of A", regardless of where you see it

modern magnet
#

I'm not saying TS does anything wrong. I just think it can do something better

normal niche
#

it's like a function that returns string is enough information to tell you that it might return string | 'some specific string' | 'another specific string' | ...

modern magnet
#

(A|B)[] is better than A[] ๐Ÿ™‚

normal niche
#

i'm trying to explain that they're exactly the same. they mean the same thing in the type system

#

oh, maybe you just mean for how TS displays that type in a type hint?

modern magnet
#

because where I want to do this in the code is "far away" from this function

normal niche
#

i'm saying you can always do that for any function that returns an A. the | B does not change anything about that. does that make sense?

modern magnet
#

well, if the function would look like

const myFunc = () => {
  return [
    ...([] as A[])
  ]
};

then trying to see if an element from this function contains a property in B is completely useless, but other developers now are in the belief it can return a B, but it never will.

normal niche
#

why is it completely useless? this is a valid implementation of a myFunc that returns just A[]:

vast brookBOT
#
interface A {}
interface B extends A {
  propertyThatOnlyBHas: true
}

const myFunc = (): A[] => {
  const b: B = { propertyThatOnlyBHas: true }
  return [b]
}
normal niche
#

a caller of such a function may want to narrow the array element to B still

modern magnet
#

Alright I think I know why TS does this, still not completely happy I could end up with dead code but I accept it ๐Ÿ™‚

normal niche
#

i'm curious what actual code that led to this question is doing. there may be a different way to model things that doesn't involve one of the types being a subtype of the other, which sounds like it might make your life easier. i'd be happy to offer suggestions if you feel like sharing the real code/use case

modern magnet
#

I think it's too tied to our business logic but in our app we have A and then a bunch of interfaces extend A, B C or D.

But I know my function can only return A or B. TS should also be able to know this. So if TS says the function return only A[] then any developer think it's an A or anything else that implements A.

#

But I have no idea if it's completely empty, only filled with A, only filled with B, or filled with both A and B. But I'm certain it can't ever contain C or D.

#

but I still of course see your point that the function "could" return C or B "disguised", maybe not the correct term ๐Ÿ™‚

normal niche
#

that's helpful even at such a high level. maybe you could introduce a new subtype of A (let's call it E) that models specifically what your function returns when it's not returning B (in a way that's narrower than the vague constraints A imposes). then the return type would be (B | E)[], and since B and E have no overlap and are both at the same "level" as C and D it'll help make it more clear what callers can expect to get back

#

sounds like you may also be interested in learning about discriminated unions if you don't already know about them. the idea is that you make sure all of your types in the union can have no overlap by tagging them in an explicit way, which makes narrowing much nicer

#

e.g. you could just check thingy.type === 'B' or somesuch rather than looking at other properties to infer its B-ness

modern magnet
#
  const filteredPlanTransactionsOnOrganizationalUnits = useMemo<
    (Transaction | ScenarioTransaction)[]
  >(() => {
    return [
      ...planActualTransactions, // Transaction[]
      ...activeScenariosTransactions, // (ScenarioTransaction | ReferableScenarioTransaction)[]
      ...nonScenarioTransactions, // Transaction[]
      ...temporaryTransactions, // Transaction[]
    ].filter(filterTransactionsByOrganizationalUnits);
  }, [
    planActualTransactions,
    activeScenariosTransactions,
    nonScenarioTransactions,
    temporaryTransactions,
    filterTransactionsByOrganizationalUnits,
  ]);

This is the code I'm having trouble with. You can see here I've asserted useMemo to return (Transaction | ScenarioTransaction)[](useMemo is a react hook).

My goto would be to just leave out the generic so TS can infer it.
However by doing so the return type looks like this:

const filteredPlanTransactionsOnOrganizationalUnits: (({
    versionId: string;
    versionName: string;
    accountNumber: string;
    ccCode: string;
    transactionDate: string;
    amount: number;
    transactiondescr?: string | undefined;
    type: "1" | "2";
    customerCode: string;
    ... 4 more ...;
    employeeCode: string;
} & {
    ...;
}) | ({
    versionId: string;
    versionName: string;
    accountNumber: string;
    ccCode: string;
    transactionDate: string;
    amount: number;
    transactiondescr?: string | undefined;
    type: "1" | "2";
    customerCode: string;
    ... 4 more ...;
    employeeCode: string;
} & {
    ...;
}))[]

In my perfect world the inferred return type would be (Transaction | ScenarioTransaction | ReferableScenarioTransaction)[]. What happened now is that someone forgot to add ReferableScenarioTransaction to the generic when activeScenariosTransactions was changed to being a ScenarioTransaction | ReferableScenarioTransaction