【问题标题】:TypeScript: Generics: Is there a safe and strict way to pick properties? (which must exist on source AND target type)TypeScript: Generics: Is there a safe and strict way to pick properties? (which must exist on source AND target type)
【发布时间】:2022-12-01 19:12:59
【问题描述】:

I want to transform type A to type B, but I want to get warnings if I choose properties which are not defined on both types.

type Source = {
  id: number;
  foo: string;
  onlyOnSource: string;
}

type Target = {
  id: number;
  foo: string;
  onlyOnTarget?: string;
}


const source: Source = {
  id: 1,
  foo: 'hello',
  onlyOnSource: 'test',
}

const transformer1 = (response?: Source): Partial<Target> => {
  return {
    id: response?.id,
    foo: response?.foo,
    // wrong: undefined, // cool. error is catched: Object literal may only specify known properties, and 'wrong' does not exist in type 'Partial<Target>'
    // onlyOnTarget: response?.onlyOnTarget, // cool. error is catched: Property 'onlyOnTarget' does not exist on type 'Source'
  };
};

...overall this works best. But I have to write all the properties twice (id, foo). And it's possible to assign them wrong (i.e. id: response?.foo)

So I tried a generic "safePick" function (which is similar to lodash/pick, but warns when properties do not exist)

const safePick = <T, K extends keyof T>(source?: T, ...keys: K[]): Partial<Pick<T, K>> => {
      if (!source) return {};
      const target: Partial<Pick<T, K>> = {};
      keys.forEach(key => {
        if (source[key] !== undefined) target[key] = source[key];
      });
      return target;
    };

const transformer2 = (response?: Source): Partial<Target> => {
  // return safePick(response, 'id', 'foo', 'wrong'); // cool. error is catched: Argument of type '"wrong"' is not assignable to parameter of type 'keyof Source'
  return safePick(response, 'id', 'foo', 'onlyOnSource'); // WRONG! 'onlyOnSource' should not be alowed on type Target!
};

But that's not complaining when I assign the property "onlyOnSource" to type Target. :(

Even when I remove the "Partial" part, it still doesn't care.

But I want this to fail with a warning. Ideally in a generic way, so I don't have to pass all the keys of both types manually.

I tried many things. Even using "zod". But couldn't find a nice solution. Does anyone have an idea?

【问题讨论】:

  • Why do you make the input optional (response?: Source): Partial&lt;Target&gt;? What happen if you don't pass an input with this function?
  • Even without the optional and the Partial it doesn't work. (I need this because the transformer has to transform a state to a form-object. If the form is not yet filled - for example during first creation - the state is not yet there, that's why a Partial is enough at this state. The validation that everything was filled is performed later)
  • This is because Partial&lt;Pick&lt;Source, "id" | "foo" | "onlyOnSource"&gt;&gt; and Partial&lt;Target&gt; are assignable to each other

标签: typescript typescript-generics


【解决方案1】:

You have to put the Target type to the function also as argument. This is because object with extra fields will be subtype of return type of Patrial&lt;Target&gt; so it won't fail as you want.

Then use that type to compute some wrong type if there is extra field.

const safePick = <Target>() => {
    return <T, K extends keyof T>(source?: T, ...keys: K[]): TargetType<Target, K, T> => {
        // @ts-ignore
      if (!source) return {};
      const target: Partial<Pick<T, K>> = {};
      keys.forEach(key => {
        if (source[key] !== undefined) target[key] = source[key];
      });
        // @ts-ignore
      return target;
    };
}

The type-level function that will compute wrong function is this one:

type TargetType<Target, K extends keyof T, T> = Exclude<K, keyof Target> extends never ? Partial<Pick<T, K>> : void

Here, you check what is the result, if you take all keys of Source and exclude from it all keys of Target. If there is never, that means there is no extra fields in Source, and you can safely return your desired type. Otherwise, return "fail" type void, that will cause compilation error.

const transformer2 = (response?: Source): Partial<Target> => {
  return safePick<Target>()(response, 'id', 'onlyOnSource'); // WRONG! 'onlyOnSource' should not be alowed on type Target!
};

This will then result in this:

Type 'void' is not assignable to type 'Partial<Target>'.ts(2322)

【讨论】:

    猜你喜欢
    • 2022-12-01
    • 2022-12-26
    • 2022-12-26
    • 2022-07-14
    • 2022-12-02
    • 2022-12-02
    • 2022-12-02
    • 2022-12-02
    • 2022-12-26
    相关资源
    最近更新 更多