#How do I make a function that accepts an intersected interface with a generic parameter?

98 messages · Page 1 of 1 (latest)

tropic sparrowBOT
#
bawdyinkslinger#0

Preview:```ts
export const versionList = [
302,
402, // and many more
] as const

export type VersionTuple = typeof versionList
export type AnyVersion = VersionTuple[number]

export interface ConfigMoment<T extends AnyVersion> {
variables: Variables<T>
}

export type VersionVariableMap
...```

crude orbit
#

Here is the important excerpt of the playground link:

// this is what I want to pass in to the deleteVariable function
declare const configMoment: ConfigMoment<302> & ConfigMoment<402>

// this is the function I want to be able to call with some type safety:
export function deleteVariable<T extends AnyVersion>(
  configMoment: ConfigMoment<T>,
  variableName: keyof ConfigMoment<T>['variables'] & string
): void {
  delete configMoment.variables[variableName];
}

// and this is how I call it:
deleteVariable(configMoment, 'foo');
// And this is the error message: Argument of type 'string' is not assignable to parameter of type 'never'.(2345)
split niche
#

I'll have a look but immediate thought when reading this question is "correspondence problem"

#

Oh yes, correspondence problem

#

!*:correspondence

#

!:correspondence

tropic sparrowBOT
#
retsam19#0
`!retsam19:correspondence-problem`:

There's a particular pattern that is safe but hard for the Typescript compiler to handle, which I call the "correspondence problem":

const functionsWithArguments = [
  { func: (arg: string) => {}, arg: "foo" },
  { func: (arg: number) => {}, arg: 0 },
];

for (const { func, arg } of functionsWithArguments) {
  func(arg);
//     ^^^
// Argument of type 'string | number' is not assignable to parameter of type 'never'.
//   Type 'string' is not assignable to type 'never'.
}

The problem is that func is typed as (x: string) => void | (x: number) => void and arg is string | number, but the compiler can't prove that they "correspond": that, for example, arg is only a string when func accepts strings.

As far as the type are concerned, arg could be number, and func could be (arg: string) => void, and that would be a type-error. It's easy for us to see that that won't happen, but that requires understanding the program at a higher-level than the level the compiler operates.

Depending on the specifics there's sometimes clever fixes, but usually I recommend using a type assertion and ignoring the issue:

func(arg as never);
crude orbit
split niche
#

Yes

crude orbit
#

and even though the compiler can't check this, it'll end up calling the right function?

split niche
#

You can't do that

#

TS does not have ad hoc polymorphism

#

unless I misunderstand your question

#

(removed a misleading message of mine)

crude orbit
split niche
#

TS only puts types on JS code, it eventually just compiles down to JS

#

Therefore it does not have any runtime semantics, what you put in your types cannot have any effect on runtime decisions

#

However, I'm not sure what you mean with "the right function"

crude orbit
#

I was imagining that the func value was overloaded, but that isn't the case

#

What would happen if it was overloaded? undefined?

split niche
#

if you were to call func(foo as never) with an overloaded func it would just take the first overload, because never is a subtype of everything.

#

But that wouldn't really matter because casting to never is pretty much throwing all your type safety out the window

crude orbit
split niche
#

Working on it personally

#

Right now even 😄

#

Whether that would get merged is a completely different question though

#

I'm just doing experiments with a possible solution in my own fork

crude orbit
#

It's weird: I feel like I see other people create types that are so sophisticated I can't even understand them, but here I feel like I'm trying to do something fundamental and it can't be done

#

I brought this up before but the most difficult part of advanced types to me is knowing what is and isn't possible

split niche
#

Yes, practically every question I answer where I have to answer with "can't be done" is this exact problem

crude orbit
#

Oh well. I wish you luck!

split niche
#

You're just running into a fundamental limitation

#

Thank you!

#

You too!

split niche
#

Yes

#

There's 2 common forms of it:

crude orbit
#

ah

split niche
#

The one you ran into, which most people find when they try to make some kind of "switch statement" component for multiple different React components

#

And then there's the one where people try to do function createSomething<T extends PossibleObjects>(type: T['type']): T { switch (type) { ... } }

#

That gives a different error but it's the same problem, just its contravariant complement

#

I'm closing in on the solution though.

crude orbit
split niche
#

The fact that I'm working on it is also why I spotted it immediately

crude orbit
#

In my case, should I just make that second argument of deleteVariable a string?

#

or is it providing some sort of type safety internally to the function?

split niche
#

You have to break type safety for that parameter somewhere, so I guess that choice depends on what you prefer

#

If this deleteVariable is a "public api" then it may be more ergonomic to indeed type it as string, and then do some cast inside the function. Otherwise every call site will have to use as never

#

That as never in that snippet mostly addresses the common case where this problem occurs, in a for loop over a constant list of config objects

crude orbit
#

I guess internally... If I forgot I did this and came across as never , I'd be more confused than if I saw arg2:string

split niche
#

Makes sense

crude orbit
#

I think I get it (unless that last statement was incorrect)

split niche
#

Not sure what autopath is

#

But I did manage to move your cast inside the function while keeping a type safe interface:

tropic sparrowBOT
#
angryzor#0

Preview:```ts
export const versionList = [
302,
402, // and many more
] as const

export type VersionTuple = typeof versionList
export type AnyVersion = VersionTuple[number]

export interface ConfigMoment<T extends AnyVersion> {
variables: Variables<T>
}

export type VersionVariableMap
...```

split niche
#

More legible version:

tropic sparrowBOT
#
angryzor#0

Preview:```ts
export const versionList = [
302,
402, // and many more
] as const

export type VersionTuple = typeof versionList
export type AnyVersion = VersionTuple[number]

export interface ConfigMoment<T extends AnyVersion> {
variables: Variables<T>
}

export type VersionVariableMap
...```

crude orbit
#

Thank you, let me check this out

#

trippy, but I think I get it

split niche
#

It's basically just generating a set of overloads

crude orbit
#
  ...[configMoment, variableName]: { // a tuple of two, flattened so as to be 2 arguments
    [T in AnyVersion]: [ // T = 302 | 402 | ...
      configMoment: ConfigMoment<T>,
      variableName: keyof ConfigMoment<T>['variables'] // variableName must be a key of the configMoment
    ];
  }[AnyVersion] // this... I'm not too sure about. It looks like it's passing in `302 | 402 | ...` to this, to get every possibility of T?
split niche
#

// this... I'm not too sure about. It looks like it's passing in 302 | 402 | ... to this, to get every possibility of T?

#

It's a common method of distributing over the items in a union

#

There's 2 methods, either use distributive conditionals or use a mapped type that you immediately dereference

crude orbit
#
[T in AnyVersion]: [
      configMoment: ConfigMoment<302>,
      variableName: keyof ConfigMoment<302>['variables'] // variableName must be a key of the configMoment
    ] | [T in AnyVersion]: [
      configMoment: ConfigMoment<402>,
      variableName: keyof ConfigMoment<402>['variables'] // variableName must be a key of the configMoment
    ]```
#

I meant it's getting this, sorry

split niche
#

{ [K in 'a' | 'b']: K[] }['a' | 'b'] gives you 'a'[] | 'b'[]

#

Once that is clear the rest follows more easily:

#

It generates a union of tuples with the allowed combinations of parameters

crude orbit
#

or is that wrong?

split niche
#

It's close but the T in part is weird

#
| [configMoment: ConfigMoment<302>, variableName: keyof ConfigMoment<302>['variables']]
| [configMoment: ConfigMoment<402>, variableName: keyof ConfigMoment<402>['variables']]
#

It generates that

#

The tuples are named tuples because TS can use those to suggest the parameter names in autocomplete

crude orbit
crude orbit
split niche
#

nope

#

it would just see that as an anonymous destructured array

#

and it would show the parameter names param_0 and param_1 or something like that

#

You can't rely on those destructured names because they are just where your inputs are assigned to, they may not map directly to the overloads you generate.

#

Example:

#
function (...[arg1, arg2]: [string] | [number, boolean]) {
}
#

now these 2 overloads may not have anything to do with each other anymore, you may want a different param name for the first param in either overload

#

and the second param doesn't even exist in both overloads

#

Through the named types you can specify this more correctly:

function (...[arg1, arg2]: [name: string] | [index: number, reverse: boolean]) {
}
crude orbit
#

I'm sorry I'm just rusty on all this stuff because it's been a few weeks since I've had to work with a complex type

#

Thank you again for all the help!

split niche
#

It's ok

crude orbit
#

!solve

split niche
#

You're welcome!

crude orbit
#

!solved

#

hm?

#

!resolved