简体   繁体   中英

Typescript Mysterious Intersection

TLDR: Playground Repro

In my application, I'm defining multiple form modules which look roughly like:

const firstModule = {
    name: 'firstModule',
    mutation: () => {
        return (opts: {variables: {firstModuleArg: string}}) => {} 
    }
}

const secondModule = {
    name: 'secondModule',
    mutation: () => {
        return (opts: {variables: {secondModuleArg: number}}) => {} 
    }
}

As you can see, each mutation function returns a function that expects a particularly shaped variables field.

Usage of each module directly works just fine:

firstModule.mutation()({ variables: { firstModuleArg: 'test' } }); => ok

secondModule.mutation()({ variables: { secondModuleArg: 123 } }); => ok

However, I'm also creating a central registry of these forms so that I can look them up from elsewhere like so:

const forms = {
    firstModule,
    secondModule
}


const getFormConfig = (root: 'firstModule' | 'secondModule') => {
    const rootObj = forms[root];

    return rootObj;
}

This is where the issue is.. When I then try to refer to a single member of the combined form object, it seems like Typescript is automatically creating an intersection of the variables fields and throwing the following error:

const { mutation: firstModuleMutation } = getFormConfig('firstModule');

firstModuleMutation()({ variables: { firstModuleArg: '1234' } });

打字稿错误

I imagine I'm missing something fairly simple here, but was hoping to get some insight into how to get the ideal behavior (when I specifically retrieve the firstModule , I only want it to validate the variables field from that module). Please let me know if there's any other information I can provide.

Thanks!

When a function is defined this way, TypeScript loses the relation between your module name and your mutation return type.

You can either use function overloads or define your function using type parameters. Since the first solution was already provided, let me present the second approach. Its advantage is that it scales indefinitely. If you decide to extend your model, it will just work, whereas with overloads you would have to update them every time your model changes.

We will need a few commonly used helpers first.

type ValueOf<T> = T[keyof T];
type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U;

Your domain model:

/**
 * Type aliases.
 */
type Forms = typeof forms;
type Module = ValueOf<Forms>;

/**
 * The return type for `getFormConfig`.
 */
type TransformedModule<T extends Module> = Overwrite<T, { mutation: ReturnType<T['mutation']> }>;

The final solution:

export function getFormConfig<K extends keyof Forms>(arg: K) {
  const module = forms[arg];

  return ({ ...module, mutation: module.mutation() }) as TransformedModule<Forms[K]>;
}

Usage:

getFormConfig('firstModule').mutation({ variables: { firstModuleArg: 'foo' } })
getFormConfig('secondModule').mutation({ variables: { secondModuleArg: 42 } });

You could help the compiler with overloads:

function getFormConfig(root: 'firstModule'):
    typeof firstModule & { mutation: ReturnType<typeof firstModule.mutation> }
function getFormConfig(root: 'secondModule'):
    typeof secondModule & { mutation: ReturnType<typeof secondModule.mutation> }
function getFormConfig(root: 'firstModule' | 'secondModule') {
    const rootObj = forms[root];

    const mutation = rootObj.mutation();
    return {...rootObj, mutation}
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM