#Automatic types possible?

180 messages · Page 1 of 1 (latest)

zealous lark
#

I have this array which is used to define how a portion of an application works (ie this defines a portion of config)

const injectableServices: Array<keyof SymbolDefinitionInjectMap> = ["webServices", "displayProvider", "assetContext"];

I then have the implementation layer which makes use of the configured object.

const [webServices, displayProvider, assetContext] = getServices(inject, args) as [
        SymbolDefinitionInjectMap["webServices"],
        SymbolDefinitionInjectMap["displayProvider"],
        SymbolDefinitionInjectMap["assetContext"],
      ];

Now, this works. But, injectableServices can be anywhere from 0 to 50 items. And I'd rather not manually type them out by running getServices. Is there any way to automatically type the results? The items in injectableServices have defined types and will ALWAYS be returned in the same order from getServices.

frail kraken
#

could you share a more complete example in the playground? particularly the signature of getServices

#

i'm guessing inject is the same as injectableServices but i'm not sure

zealous lark
#

They are the same

#
  function getServices<T extends keyof SymbolDefinitionInjectMap>(
    inject: Array<T>,
    args: SymbolDefinitionArgs<T>,
  ): any {
    return inject.forEach((key, index) => {
      return index !== -1 ? args[index] : undefined;
    });
  }

  function createInitMethod<T extends keyof SymbolDefinitionInjectMap>(
    inject: Array<T>,
  ): (scope: VisionScope, elem: JQuery, ...args: SymbolDefinitionArgs<T>) => void {
    return function (scope: VisionScope, elem: JQuery, ...args: SymbolDefinitionArgs<T>): void {
      const [webServices, displayProvider, assetContext] = getServices(inject, args) as [
        SymbolDefinitionInjectMap["webServices"],
        SymbolDefinitionInjectMap["displayProvider"],
        SymbolDefinitionInjectMap["assetContext"],
      ];

      console.log(assetContext);
      // Additional implementation logic here
    };
  }

  symbolVis.prototype.init = createInitMethod(injectableServices);
frail kraken
#

thanks. yes this is definitely possible, and you should be able to avoid the need to assert at all. gimme a moment

zealous lark
#

Okay cool, thanks

frail kraken
#

it's hard to be sure without knowing what all of those types are, but i think you could do something like this:

#
type Services<T extends readonly keyof SymbolDefinitionInjectMap[]> = {
  [K in keyof T]: SymbolDefinitionInjectMap[T[K]]
}

function getServices<T extends readonly string[]>(
  inject: T,
  args: SymbolDefinitionArgs<T[number]>,
): Services<T> {
  return inject.forEach((key, index) => {
    return index !== -1 ? args[index] : undefined;
  });
}
zealous lark
#

What all those types are, are you referring to the map?

frail kraken
#

not sure if this is what you're asking about, but Services is a mapped type

#

oh wait

zealous lark
#
declare type SymbolDefinitionInjectMap = {
  $anchorScroll: ng.IAnchorScrollService;
  $cacheFactory: ng.ICacheFactoryService;
  adHocService: InjectAdHocService;
  appClipboard: InjectAppClipboard;
  appData: InjectAppData;
  assetContext: InjectAssetContext;
frail kraken
#

i just mean i can't actually test your code because you haven't given me the definitions for SymbolDefinitionInjectMap, SymbolDefinitionArgs, etc

zealous lark
#

Example of some of the types. Some are angular services, some are custom and have their own types

#
type SymbolDefinitionArgs<T extends keyof SymbolDefinitionInjectMap> = T extends keyof SymbolDefinitionInjectMap
  ? [SymbolDefinitionInjectMap[T]]
  : never;
frail kraken
#

generally it's a good practice to give a reduced, complete, self-contained example to work with for questions like this

#

it's okay though, just see if what i provided works?

zealous lark
#

It also didn't like the readonly keyword

#

'readonly' type modifier is only permitted on array and tuple literal types.

frail kraken
#

did you forget the [] at the end of the constraint?

zealous lark
frail kraken
#

the trick here is to be generic over the whole array type, not just the element type

#

copy exactly this:

type Services<T extends readonly keyof SymbolDefinitionInjectMap[]> = {
  [K in keyof T]: SymbolDefinitionInjectMap[T[K]]
}

don't stick parentheses in there

zealous lark
#

Ah, damn prettier

#

Didn't make a difference though

frail kraken
#

i think i'm gonna need a more complete example to help you debug. otherwise i'm just guessing

#

can you throw your code into the playground?

zealous lark
#

Alright, let me see what I can do

frail kraken
#

here's a simple example of the strategy i'm suggesting:

haughty hemlockBOT
#
type Stuff = {
  a: string
  b: number
  c: boolean
}

type Things<T extends readonly (keyof Stuff)[]> = {
  [K in keyof T]: Stuff[T[K]]
}

declare function f<const T extends readonly (keyof Stuff)[]>(inputs: T): Things<T>

const output = f(['a', 'c', 'b'])
//    ^? - const output: readonly [string, boolean, number]
frail kraken
#

(i guess i lied about not needing parens, sorry)

zealous lark
#

Okay, this link is really long

frail kraken
#

try to reduce the example down if you can. i doubt all of the details are really relevant to your question

#

but if the link is still too long there's a link shortener plugin. check the right sidebar

zealous lark
frail kraken
#

something like this maybe?

haughty hemlockBOT
#
mkantor#0

Preview:ts declare type SymbolDefinitionInjectMap = { // Some types are really long, so here's a small, simple one // There's about 50 total that are in this map routeParams: InjectRouteParams; webServices: InjectRouteParams; // duplicated to reduce errors in this example ...

frail kraken
#

the as Services<T> in getServices is unfortunate, but it's needed because when you map over an array with a known length that length information is sadly lost

zealous lark
#

So that does work but puts me back to a spot I was at before, where the types returned from getServices provide a union type for each item, instead of the proper type

frail kraken
#

in your real code SymbolDefinitionInjectMap has different value types for each property, yeah?

zealous lark
#

Correct

frail kraken
#

oh i see where you're pointing that out (inside createInitMethod). what else would they be there? the caller of createInitMethod gets to decide what to pass in for inject. they could be any elements in any order

#

actually yeah, this whole thing seems unsafe because of that

#

what if they give you []?

#

then you won't have any things to destructure at all

zealous lark
#

Well no. It's defined in this code. Not by anyone else

#
 const injectableServices = ["webServices", "displayProvider", "assetContext"] as const satisfies Array<
    keyof SymbolDefinitionInjectMap
  >;
#

That's the definition.

frail kraken
#

oh, then why is it a parameter for createInitMethod?

#

gotcha. it never changes?

zealous lark
#

So I can tie in the types

#

No, it's always defined for each instance in the code. Not by a user

frail kraken
#

i think it'll be better to just refer to the constant. hang on a minute i'll do another iteration

zealous lark
#

The variable injectableServices is what I use to define the services that Angular will inject into the init function. So I define them once as a variable, and pass them into the configuration. When init is called, Angular will inject the items defined in injectableServices into the init function

frail kraken
#

what about getServices? does that function also always work with the same constant? or can different injectable services be passed to different calls of getServices?

zealous lark
#

I built getServices to iterate over injectableServices to ensure we always get the same array in the same order

#

And it pulls the injected services out from the init function

frail kraken
#

we can do that directly rather than in a function if the value of init never changes

#

which will make things much simpler

zealous lark
#

init changes

#

Well

#

init = function (scope, elem, ...args) is the function signature

#

The function doesn't change. What does change is injectableServices

#

If there's 3 items in that array, then ...args will have 3 items. If 5, then 5, if 0 then 0

frail kraken
#

okay i think we misunderstood eachother then

zealous lark
#

Each of which will be typed according to the SymbolDefinitionInjectMap

frail kraken
#

that last line where you have the createInitMethod(injectableServices) example... in your real code who is providing injectableServices? is every single call to createInitMethod going to look exactly like that (referring to the same three-element injectableServices array), or is it possible for createInitMethod to ever be called with a different argument?

zealous lark
#

Angular is providing them

#

I don't have control over that

#

I tell angular I want this array, and it provides me that

frail kraken
#

so what if for example angular decides to call createInitMethod(someOtherServices)? maybe someOtherServices only has one element, or three elements but in a different order

zealous lark
#

So from my code side, the signature is always the same

frail kraken
#

you're giving the injectableServices variable to angular, and then later it calls createInitMethod with exactly what you gave it?

zealous lark
#

I can have plugin A and plugin B.

// Plugin A
const injectableServices = ["webServices", "displayProvider", "assetContext"]

// Plugin B
const injectableServices = ["webServices"]

// without trying to extract types automatically, init looks like this:
// A
symbolVis.prototype.init(scope: VisionScope, elem: JQuery, webServices: InjectWebServices, displayProvider: InjectDisplayProvider, assetContext: InjectAssetContext)
// B
symbolVis.prototype.init(scope: VisionScope, elem: JQuery, webServices: InjectWebServices)
#

Each plugin is a physically separate file and has no reference to the other.

#

Angular treats each of them exactly the same

frail kraken
#

so in the prior example createInitMethod is for plugin A, but you would have a completely different createInitMethod implementation for plugin B?

zealous lark
#

No

#

They're the same. They're handled exactly the same

frail kraken
#

so when plugin B calls createInitMethod the body of that function cannot assume it will always get three elements back from getServices, right?

zealous lark
#

Plugin B will get back the services defined in its injectableServices

#

const injectableServices = ["webServices"]

frail kraken
#

right, but your implementation of createInitMethod assumes you have three services. and that's not the case for plugin B

zealous lark
#
// Plugin A
symbolVis.prototype.init = (scope, elem, ...args) {
  const [webServices, displayProvider, assetContext] = args;
}

// Plugin B
symbolVis.prototype.init = (scope, elem, ...args) {
  const [webServices] = args;
}
#

Right, they have a variable amount of services

#
// Plugin A
symbolVis.prototype.init = (scope, elem, ...args) {
  const [webServices, displayProvider, assetContext] = getServices(inject, args);
}

// Plugin B
symbolVis.prototype.init = (scope, elem, ...args) {
  const [webServices] = getServices(inject, args);
}
frail kraken
#

are you saying the const [webServices, displayProvider, assetContext] = part of the implementation of createInitMethod is not part of your real code?

zealous lark
#

No, it is and it works. I just don't have the right types from destructuring, unless I type them explicitly.

frail kraken
#

sorry but i am super confused

zealous lark
#
const [webServices, displayProvider, assetContext] = PV.MainelyInnovationsCommonSetup.getServices<T>(
        inject,
        args,
      ) as [
        SymbolDefinitionInjectMap["webServices"], // types are manual, can we make automatic?
        SymbolDefinitionInjectMap["displayProvider"],
        SymbolDefinitionInjectMap["assetContext"],
      ];
frail kraken
zealous lark
#

I have to specify the types, otherwise they're typed as the union of SymbolDefinitionInjectMap

#
  function createInitMethod<T extends (keyof SymbolDefinitionInjectMap)[]>(
    inject: T,
  ): (scope: VisionScope, elem: JQuery, ...args: SymbolDefinitionArgs<T[number]>) => void {
    return function (scope: VisionScope, elem: JQuery, ...args: SymbolDefinitionArgs<T[number]>): void {
      const [webServices, displayProvider, assetContext] = getServices<T>(
        inject,
        args,
      ) as [
        SymbolDefinitionInjectMap["webServices"], // types are manual, can we make automatic?
        SymbolDefinitionInjectMap["displayProvider"],
        SymbolDefinitionInjectMap["assetContext"],
      ];

      console.log(assetContext);
      // Additional implementation logic here
    };
  }

  symbolVis.prototype.init = createInitMethod(injectableServices);
#

Basically, I pass in the static array for each plugin to the createInitMethod so we can have it defined once for configuration and once for implementation

#

That then builds the return function call for the Angular system

frail kraken
zealous lark
#

So consider above Plugin A

#

Plugin B, let's say only has 1 service. That would look like this

  function createInitMethod<T extends (keyof SymbolDefinitionInjectMap)[]>(
    inject: T,
  ): (scope: VisionScope, elem: JQuery, ...args: SymbolDefinitionArgs<T[number]>) => void {
    return function (scope: VisionScope, elem: JQuery, ...args: SymbolDefinitionArgs<T[number]>): void {
      const [webServices] = getServices<T>(
        inject,
        args,
      ) as [
        SymbolDefinitionInjectMap["webServices"]
      ];

      console.log(assetContext);
      // Additional implementation logic here
    };
  }

  symbolVis.prototype.init = createInitMethod(injectableServices);
frail kraken
#

okay, so there are different implementations of createInitMethod for each plugin

zealous lark
#

The only difference is that injectableServices has 3 vs 1 items in the array

frail kraken
#

each plugin has its own createInitMethod

#

right?

zealous lark
#

I was looking to see if I could extract it out to my common class, but I probably can't

#

So I'd say that yes, each will likely have its own

frail kraken
#

so in that case i still believe that createInitMethod doesn't need to be generic

zealous lark
#

Yeah my bad, I was looking at it a bit different

frail kraken
#

or if it does it would be more of a higher-order function

#

out of curiosity what does the rest of createInitMethod typically look like? like what does it do with the services that it destructures?

zealous lark
#

If I can pass injectableServices into createInitMethod and get the right return types out from getServices, that's what I'd want

#

You're talking about the // Additional implementation logic?

frail kraken
#

yeah

zealous lark
#

Plugin-specific code and unrelated

#

It's the actual logic of each function. Do x, y, z

frail kraken
#

okay, i'm just thinking through what parts of this could be abstracted/shared

zealous lark
#

For example, getServices is actually in my common class because it can be shared. I don't think createInitMethod can be, but I can work on that later

frail kraken
#

yup i think i get it now

#

how about something more like this, then?

haughty hemlockBOT
#
mkantor#0

Preview:```ts
...
function createInitMethod(scope: VisionScope, elem: JQuery, ...args: SymbolDefinitionArgs<InjectableService>): void {
const [webServices, displayProvider, assetContext] = getServices(
injectableServices,
args,
);

console.log(assetContext);
// Additional implementation logic here
}

// Ignore error, this is how it's used with the 3rd party
symbolVis.prototype.init = createInitMethod;```

frail kraken
#

that should manage to keep track of the specific type for each service

zealous lark
#

Oooh

frail kraken
#

i guess createInitMethod should be renamed to just initMethod there

zealous lark
#

Eh, semantics

#

I technically don't even need to declare it separately

frail kraken
#

right could just be an anonymous closure

zealous lark
#

The only thing, for reference, while the editor didn't show me any errors, the line (apologizes in prettier)

const injectableServices = ["webServices", "displayProvider", "assetContext"] as const satisfies Array<
    keyof SymbolDefinitionInjectMap
  >;

actually threw some errors when compiling.

#
src/Symbol/Layout/Sidebar/sym-mi-sidebar.ts:8:90 - error TS1360: Type 'readonly ["webServices", "displayProvider", "assetContext"]' does not satisfy the expected type '(keyof SymbolDefinitionInjectMap)[]'.
  The type 'readonly ["webServices", "displayProvider", "assetContext"]' is 'readonly' and cannot be assigned to the mutable type '(keyof SymbolDefinitionInjectMap)[]'.

8   const injectableServices = ["webServices", "displayProvider", "assetContext"] as const satisfies Array<
                                                                                           ~~~~~~~~~

src/Symbol/Layout/Sidebar/sym-mi-sidebar.ts:45:5 - error TS4104: The type 'readonly ["webServices", "displayProvider", "assetContext"]' is 'readonly' and cannot be assigned to the mutable type '("assetContext" | "displayProvider" | "webServices")[]'.

45     inject: injectableServices,
       ~~~~~~

  src/Type/Extensibility/SymbolDefinition.d.ts:11:3
    11   inject?: Array<I>;
         ~~~~~~
    The expected type comes from property 'inject' which is declared here on type 'SymbolDefinition<VisionSymbolConfig, "assetContext" | "displayProvider" | "webServices">'

src/Symbol/Layout/Sidebar/sym-mi-sidebar.ts:65:7 - error TS2345: Argument of type 'readonly ["webServices", "displayProvider", "assetContext"]' is not assignable to parameter of type '(keyof SymbolDefinitionInjectMap)[]'.
  The type 'readonly ["webServices", "displayProvider", "assetContext"]' is 'readonly' and cannot be assigned to the mutable type '(keyof SymbolDefinitionInjectMap)[]'.

65       injectableServices,
         ~~~~~~~~~~~~~~~~~~


Found 3 errors in the same file, starting at: src/Symbol/Layout/Sidebar/sym-mi-sidebar.ts:8
#

I changed it to const injectableServices: Array<keyof SymbolDefinitionInjectMap> = ["webServices", "displayProvider", "assetContext"]; and it compiled fine

#

Ohh, but then I lose the types

frail kraken
#

but that's not going to work

#

yeah

#

satisfies is new-ish but not super new. what version of typescript are you using?

#

you could also ditch it and rely on usage sites to check the type

#

just do const injectableServices = [...] as const

zealous lark
#

5.2.2

frail kraken
#

hmmm satisfies should work there

#

oh wait sorry i didn't even read the error

#

you forgot a readonly somewhere

#

you want this i guess:

const injectableServices = ["webServices", "displayProvider", "assetContext"] as const satisfies readonly (keyof SymbolDefinitionInjectMap)[];
#

weird that the playground doesn't show that error?

#

also FYI ReadonlyArray<T> is a synonym for readonly T[] (just like Array<T> is a synonym for T[])

#

in case you prefer that style

#

ah i guess this was an actual change since TS 5.2. satisfies can relax the readonly-ness that as const adds. TIL

zealous lark
#

Hmm, for some reason it's saying I'm passing readonly to a mutable type

#
 const injectableServices = ["webServices", "displayProvider", "assetContext"] as const satisfies ReadonlyArray<
    keyof SymbolDefinitionInjectMap
  >;

const [webServices, displayProvider, assetContext] = PV.MainelyInnovationsCommonSetup.getServices(
      injectableServices, // <- readonly to mutable
      args,
    );

function getServices<T extends ReadonlyArray<keyof SymbolDefinitionInjectMap>>(
      inject: T,
      args: SymbolDefinitionArgs<T[number]>,
    ): InjectedServices<T> {
      return inject.map((key, index) => {
        return index !== -1 ? args[index] : undefined;
      }) as InjectedServices<T>;
    }
frail kraken
#

you probably just need to slap some readonlys around. in general it's good practice to take readonly arrays as inputs whenever you can, so people can give you arrays created with as const

#

(unless you actually do need to mutate the arrays)

#

hm, getServices looks legit to my eyes there

zealous lark
#

Oh, interesting

#

If getServices is within the same file, no issue

frail kraken
#

and PV.MainelyInnovationsCommonSetup.getServices is definitely that same getServices?

zealous lark
#

If I copy it to the common class, then that's where the issue is

#

And yes

#
PV.MainelyInnovationsCommonSetup = (function () {
    function getServices<T extends ReadonlyArray<keyof SymbolDefinitionInjectMap>>(
      inject: T,
      args: SymbolDefinitionArgs<T[number]>,
    ): InjectedServices<T> {
      return inject.map((key, index) => {
        return index !== -1 ? args[index] : undefined;
      }) as InjectedServices<T>;
    }

    return {
      getServices,
    };
})();
frail kraken
#

just checking because this trolls me sometimes: restart tsserver in VSCode? sometimes it caches old versions of files more aggressively than it should

zealous lark
#

Yup lol

#

Er

#

No, unhappy after

#

Oh ffffs

frail kraken
#

that doesn't seem right

#

does MainelyInnovationsCommonSetup have its own type annotation defined elsewhere?

zealous lark
#

Yes, and that's why

frail kraken
#

💀

#

i haven't seen that IIFE style with non-arrow function expressions in a while. it's bringing me back to the old days 😄

zealous lark
#

Since this is all browser based, I'm avoiding export & import and I'm not getting types direct from things

zealous lark
#

Yeah, it's a browser based plugin for an Angular app

#

Their code is spaghetti

#

They tell you that you can inject things into your plugins, but they don't tell you what's available, or what any of them do. And if you have any problems, they tell you it's not guaranteed to work

frail kraken
#

sounds... fun

#

i don't really know much about angular

zealous lark
#

I don't either, I typically use Vue. I've been reading their codebase to get an understanding of Angular

#

But regardless of how dumb this vendor is, I super appreciate your effort!

frail kraken
#

no problemo, happy to help

zealous lark
#

@frail kraken I have another complex scenario if you're willing to see if there's something that can be done for it

frail kraken
#

yeah, you should always feel free to just ask the question. even if i can't answer it maybe somebody else will