#TS2322 during signature overload

1 messages · Page 1 of 1 (latest)

jolly linden
#

Hello!

I have the following code:

type GenCtxResultId = GenCtxResult & { 'data-id': string }

type GenCtxResult = {
  'data-user-id': string
  'data-at': string
  'data-party': string
}

type GenCtxResultUnion = { 'nl-insert': GenCtxResult } | { 'nl-delete': GenCtxResult } | { insert: GenCtxResultId } | { delete: GenCtxResultId }
export const generateTcCtx: {
  (userData: { id: number; party: string }, line: true, insert: true, time?: number, changeId?: string): { 'nl-insert': GenCtxResult }
  (userData: { id: number; party: string }, line: true, insert: false, time?: number, changeId?: string): { 'nl-delete': GenCtxResult }
  (userData: { id: number; party: string }, line: false, insert: true, time?: number, changeId?: string): { insert: GenCtxResultId }
  (userData: { id: number; party: string }, line: false, insert: false, time?: number, changeId?: string): { delete: GenCtxResultId }
} = (userData: { id: number; party: string }, line: boolean, insert: boolean, time = Date.now(), changeId = TextTools.randomText(6)): GenCtxResultUnion => {
  return {
    [(line ? 'nl-' : '') + (insert ? 'insert' : 'delete')]: {
      'data-id': line ? undefined : changeId,
      'data-user-id': String(userData.id),
      'data-at': String(time),
      'data-party': userData.party
    }
  } as GenCtxResultUnion
}

It works perfectly, however the TS complains about the return type:

TS2322: Type '(userData: { id: number; party: string; }, line: boolean, insert: boolean, time?: number | undefined, changeId?: string | undefined) => GenCtxResultUnion' is not assignable to type '{ (userData: { id: number; party: string; }, line: true, insert: true, time?: number | undefined, changeId?: string | undefined): { 'nl-insert': GenCtxResult; }; (userData: { id: number; party: string; }, line: true, insert: false, time?: number | undefined, changeId?: string | undefined): { ...; }; (userData: { ......'.   Type 'GenCtxResultUnion' is not assignable to type '{ 'nl-insert': GenCtxResult; }'.     Property ''nl-insert'' is missing in type '{ 'nl-delete': GenCtxResult; }' but required in type '{ 'nl-insert': GenCtxResult; }'.

IDE is showing all navigations correctly.
i.e.:

generateTcCtx({ id: 1, party: 'a' }, true, true)['nl-insert'] // works
generateTcCtx({ id: 1, party: 'a' }, true, true)['nl-delete'] // error

I'm unsure why it complains about the return type when I cast it to GenCtxResultUnion.

Can you give me some tips/ideas?

Thank you very much.

jolly linden
#

A fun thing that just happened. I tried an alternative way to write this code, and it doesn't fail anymore.

export function generateTcCtx(userData: { id: number; party: string }, line: true, insert: true, time?: number, changeId?: string): { 'nl-insert': GenCtxResult };
export function generateTcCtx(userData: { id: number; party: string }, line: true, insert: false, time?: number, changeId?: string): { 'nl-delete': GenCtxResult };
export function generateTcCtx(userData: { id: number; party: string }, line: false, insert: true, time?: number, changeId?: string): { insert: GenCtxResultId };
export function generateTcCtx(userData: { id: number; party: string }, line: false, insert: false, time?: number, changeId?: string): { delete: GenCtxResultId };
export function generateTcCtx(userData: { id: number; party: string }, line: boolean, insert: boolean, time = Date.now(), changeId = TextTools.randomText(6)): GenCtxResultUnion {
  return {
    [(line ? 'nl-' : '') + (insert ? 'insert' : 'delete')]: {
      'data-id': line ? undefined : changeId,
      'data-user-id': String(userData.id),
      'data-at': String(time),
      'data-party': userData.party
    }
  } as GenCtxResultUnion
}

I thought the signature overloading works the same way here. (I'm aware about change of 'this' context) but that shouldn't play any role here. How come the first code generates TS2322 and this one doesn't?

tall veldt
#

here's a simplified example that might be easier to talk about:

final creekBOT
#
const f: { (): 'a'; (): 'b' } = (): 'a' | 'b' => 'a'
//    ^
// Type '() => 'a' | 'b'' is not assignable to type '{ (): "a"; (): "b"; }'.
//   Type '"a" | "b"' is not assignable to type '"a"'.
//     Type '"b"' is not assignable to type '"a"'.
tall veldt
#

with this way of annotating the type, the implementation signature must be assignable to every overload signature. that's not the case with function declaration style overload signatures

jolly linden
#

So the first implementation is not function overload signature at all? It's basically an object with multiple functions that have unfortunately the same name and therefore it merges them like two interfaces with the same name. Am I right? Thank you for quick reply.

tall veldt
#

no, it does describe a type for an overloaded function. for example:

final creekBOT
#
declare const overloaded: { (a: 'a'): 'apple'; (b: 'b'): 'banana' }

const apple = overloaded('a')
//    ^? - const apple: "apple"
const banana = overloaded('b')
//    ^? - const banana: "banana"
tall veldt
#

the difference has to do with the semantics of overloads defined via the function keyword. they're declaration-merged together and the type checker only verifies that the implementation signature is loosely-compatible with the overload signatures

#

whereas your original example where you were annotating a const with the overload type checks that the value is compatible with the entire overloaded function type (man, terminology sure gets confusing when talking about this stuff 😆)

#

so for example this is valid:

final creekBOT
#
const f: { (): 'a'; (): 'b' } = (() => 'a') as { (): 'a'; (): 'b'; (): 'c' }
tall veldt
#

because the value (the right hand side of the assignment) is now an overloaded function type whose overload signatures cover what is required by the type annotation