Home > Blockchain >  Type 'string[]' is not assignable to type '("toString" | "valueOf"
Type 'string[]' is not assignable to type '("toString" | "valueOf"

Time:08-17

I have two properties on an object being passed into a function. I want to constrain the deps property on one object's properties to be a key of the other object.

E.g.:

const config = {
  modules: {
    bar: {
      baz: true,
      deps: ['foo', 'notADep'],
      factory: () => Promise.resolve(() => {}),
    },
  },
  services: {
    foo: { factory: () => Promise.resolve(() => {}) },
  },
};

In the above config, foo is valid, but notADep is not valid.

Here's my code:

function createApp<T extends Config>(config: T) {}

export type Service<T> = {
  factory: () => Promise<() => void>;
  deps?: (keyof T & string)[] | undefined;
}

export type Services<T = any> = Partial<Record<keyof T, Service<any>>>;

export type Modules<T = any> = Record<string, {
  baz: boolean,
} & Service<keyof T>>

interface Config {
  modules: Modules;
  services: Services;
}

I'm getting an error on the config parameter that

Argument of type '{ modules: { bar: { baz: boolean; deps: string[]; factory: () => Promise<() => void>; }; }; services: { foo: { factory: () => Promise<() => void>; }; }; }' is not assignable to parameter of type 'Config'.
  Types of property 'modules' are incompatible.
    Type '{ bar: { baz: boolean; deps: string[]; factory: () => Promise<() => void>; }; }' is not assignable to type 'Modules<any>'.
      Property 'bar' is incompatible with index signature.
        Type '{ baz: boolean; deps: string[]; factory: () => Promise<() => void>; }' is not assignable to type '{ baz: boolean; } & Service<string | number | symbol>'.
          Type '{ baz: boolean; deps: string[]; factory: () => Promise<() => void>; }' is not assignable to type 'Service<string | number | symbol>'.
            Types of property 'deps' are incompatible.
              Type 'string[]' is not assignable to type '("toString" | "valueOf")[]'.
                Type 'string' is not assignable to type '"toString" | "valueOf"'.

Playground

CodePudding user response:

I don't know if I understand you correctly but you could probably do something like this playground


export type Factory = {
  factory: () => Promise<() => void>;
}

export type Services<T extends string[] = []> = { [K in T[number]]: Factory };
export type Dependencies<T extends string[] = []> = { deps: T, }

export type Modules<ModuleKeys extends string[] = [], Context extends object = {}> =
  { [M in ModuleKeys[number]]: Factory & Context }


interface Config<ModuleKeys extends string[] = [], ServiceKeys extends string[] = [], Context extends object = {}> {
  modules: Modules<ModuleKeys, Context & Dependencies<ServiceKeys>>;
  services: Services<ServiceKeys>;
}


const config: Config<["bar"], ["foo"], { baz: boolean }> = {
  modules: {
    bar: {
      baz: true,
      deps: ['foo', 'notADep'], // error!
      factory: () => Promise.resolve(() => { }),
    },
  },
  services: {
    foo: { factory: () => Promise.resolve(() => { }) },
  },
};

Edit: So the problem with your solution are these lines:

export type Service<T> = {
  factory: () => Promise<() => void>;
  deps?: (keyof T & string)[] | undefined;
}

With (keyof T & string)[] you are telling typescript that every key of T is valid that is a string. Everything in Typescript or Js is an object. For example keyof number & string will resolve to "toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString". This will also happen if you use a string, object...

If you only want to get the a union of all used keys. You should use a tuple and indexing it with tuple[number] to receive nothing else but the values you want.

//e.g.
type Tuple = ["a","b"]
type Union = Tuple[number] // "a"|"b"

So why does this work? Remember everything is an object? An array is an object aswell and but all the keys for the values are numbers. So with Tuple[number]you are telling typescript to get all values with a number key.

Ps: You can also do Tuple[string] to get all method on an array My english is trash, so if you don't understand a thing, just ask and I'll try to help you

CodePudding user response:

You can achieve this by using the builder patter, Vue does this to infer props types.

It relies on inferred generic types to reuse them and self constrain the given arguments.

M and S refers to the properties modules and services of the given parameter

playground

export type Factory = {
  factory: () => Promise<() => void>;
}

type Service<M, S> = {
  factory: () => Promise<() => void>
  // deps are any keys of the modules or services
  deps?: (keyof S | keyof M)[]
}

type Module<M, S> = {
  baz: boolean
} & Service<M, S>

type Config<M, S> = {
  modules: {
    // For every property of M, inform it's a module with the services context (S)
    [K in keyof M]: Module<M, S>
  }
  services: {
    [K in keyof S]: Service<M, S>
  };
}

// The function will automatically infer the given types
function createApp<M, S>(config: Config<M, S>): Config<M, S> {
  return config
}

createApp({
  modules: {
    bar: {
      baz: true,
      // (property) deps?: ("bar" | "foo" | "plop")[] | undefined
      deps: ['foo', 'notADep'], // error!
      //            ^ Error is located on the string, not the whole line
      factory: () => Promise.resolve(() => { }),
    },
    plop: {
      baz: true,
      deps: ['foo', 'notADep'], // error!
      factory: () => Promise.resolve(() => { }),
    },
  },
  services: {
    foo: {
      factory: () => Promise.resolve(() => { }),
      // (property) deps?: ("bar" | "foo" | "plop")[] | undefined
      deps: [],
    },
    bar: {
      factory: () => Promise.resolve(() => { }),

    },
  },
});

  • Related