如何编写一个辅助函数,仅接受根据先前属性指定的值的特定属性?

huangapple go评论59阅读模式
英文:

How to type a helper function that accepts only certain properties based on what value was specified for previous property?

问题

I'm trying to build an alias-builder helper function that would help me know which properties are accepted for a specific, selected table.

Suppose the following:

interface ITableBase {
  id: number;
}

interface ITableA extends ITableBase {
  name: string;
}

interface ITableB extends ITableBase {
  hobbies: string[];
}

const tableAliases = {
  ENVIRONMENT_A: {
    TABLE_A: (specificty: number) => `__table_a__${specificty}__`,
    TABLE_B: (specificty: number) => `__table_b__${specificty}__`,
  }
}

const generateTableAlias = ({
  alias,
  property,
  specificty = 1
}: {
  alias: keyof typeof tableAliases.ENVIRONMENT_A,
  property: string;
  specificty?: number
}) => `${tableAliases.ENVIRONMENT_A[alias](specificty)}.${property}`

I would like TypeScript to recognize the following:

const tableAlias1 = generateTableAlias({ alias: 'TABLE_A', property: 'hobbies' }) // ts error
const tableAlias2 = generateTableAlias({ alias: 'TABLE_A', property: 'name' }) // no error

Is it possible to accomplish something like this?

英文:

I'm trying to build an alias-builder helper function that would help me know which properties are accepted for a specific, selected table.

Suppose the following:

interface ITableBase {
  id: number;
}

interface ITableA extends ITableBase {
  name: string;
}

interface ITableB extends ITableBase {
  hobbies: string[];
}

const tableAliases = {
  ENVIRONMENT_A: {
    TABLE_A: (specificty: number) => `__table_a__${specificty}__`,
    TABLE_B: (specificty: number) => `__table_b__${specificty}__`,
  }
}

const generateTableAlias = ({
  alias,
  property,
  specificty = 1
}: {
  alias: keyof typeof tableAliases.ENVIRONMENT_A,
  property: string;
  specificty?: number
}) => `${tableAliases.ENVIRONMENT_A[alias](specificty)}.${property}`

I would like typescript to recognize the following:

const tableAlias1 = generateTableAlias({ alias: 'TABLE_A', property: 'hobbies' }) // ts error
const tableAlias2 = generateTableAlias({ alias: 'TABLE_A', property: 'name' }) // no error

Is it possible to accomplish something like this?

答案1

得分: 3

以下是翻译好的部分:

"First you'll need to tell the compiler about the relationship between the strings "TABLE_A"/"TABLE_B" and the types ITableA/ITableB. The easiest way to do that is with a helper interface, since object types naturally encapsulate such relationships as key-value pairs:

interface TableMapping {
    TABLE_A: ITableA;
    TABLE_B: ITableB;
}

As an aside, once you have this type you can use it to verify that tableAliases has all the properties you expect:

const tableAliases = {
    ENVIRONMENT_A: {
        TABLE_A: (specificty: number) => `__table_a__${specificty}__`,
        TABLE_B: (specificty: number) => `__table_b__${specificty}__`,
    }
} satisfies {
    ENVIRONMENT_A: Record<keyof TableMapping, (spty: number) => string>
}

where I'm using the satisfies operator to check the type without widening it. If you are missing a property in tableAliases, the discrepancy should result in an error (so if you add things to TableMapping you should get a warning to remind you to make analogous changes to tableAliases). This isn't strictly necessary, but it is probably helpful.

Anyway, now you want generateTableAlias to be generic in the type K of the alias property of the input, as follows:

const generateTableAlias = <K extends keyof TableMapping>({
    alias,
    property,
    specificty = 1
}: {
    alias: K,
    property: string & keyof TableMapping[K];
    specificty?: number
}) => `${tableAliases.ENVIRONMENT_A[alias](specificty)}.${property}`

The compiler will use the alias property of the argument to infer K. Once this happens, the compiler will then check that the property property is of type string & keyof TableMapping[K], meaning it must be a string, and it must also be one of the keys of TableMapping[K], an indexed access type corresponding to the property type of TableMapping whose key is of type K. So if K is "TABLE_A", then property must be of type string & keyof TableMapping["TABLE_A"], which is string & keyof ITableA, which is string & ("id" | "name"), which is just "id" | "name". And the analogous check is done for "TABLE_B".

Let's test it out:

const tableAlias1 = generateTableAlias(
   { alias: 'TABLE_A', property: 'hobbies' } // error!
// ------------------> ~~~~~~~~
// Type '"hobbies"' is not assignable to type '"name" | "id"'.
);

const tableAlias2 = generateTableAlias(
   { alias: 'TABLE_A', property: 'name' }
); // okay

Looks good; this is the behavior you wanted."

英文:

First you'll need to tell the compiler about the relationship between the strings &quot;TABLE_A&quot;/&quot;TABLE_B&quot; and the types ITableA/ITableB. The easiest way to do that is with a helper interface, since object types naturally encapsulate such relationships as key-value pairs:

interface TableMapping {
    TABLE_A: ITableA;
    TABLE_B: ITableB;
}

As an aside, once you have this type you can use it to verify that tableAliases has all the properties you expect:

const tableAliases = {
    ENVIRONMENT_A: {
        TABLE_A: (specificty: number) =&gt; `__table_a__${specificty}__`,
        TABLE_B: (specificty: number) =&gt; `__table_b__${specificty}__`,
    }
} satisfies {
    ENVIRONMENT_A: Record&lt;keyof TableMapping, (spty: number) =&gt; string&gt;
}

where I'm using the satisfies operator to check the type without widening it. If you are missing a property in tableAliases, the discrepancy should result in an error (so if you add things to TableMapping you should get a warning to remind you to make analogous changes to tableAliases). This isn't strictly necessary, but it is probably helpful.


Anyway, now you want generateTableAlias to be generic in the type K of the alias property of the input, as follows:

const generateTableAlias = &lt;K extends keyof TableMapping&gt;({
    alias,
    property,
    specificty = 1
}: {
    alias: K,
    property: string &amp; keyof TableMapping[K];
    specificty?: number
}) =&gt; `${tableAliases.ENVIRONMENT_A[alias](specificty)}.${property}`

The compiler will use the alias property of the argument to infer K. Once this happens, the compiler will then check that the property property is of type string &amp; keyof TableMapping[K], meaning it must be a string, and it must also be one of the keys of TableMapping[K], an indexed access type corresponding to the property type of TableMapping whose key is of type K. So if K is &quot;TABLE_A&quot;, then property must be of type string &amp; keyof TableMapping[&quot;TABLE_A&quot;], which is string &amp; keyof ITableA, which is string &amp; (&quot;id&quot; | &quot;name&quot;), which is just &quot;id&quot; | &quot;name&quot;. And the analogous check is done for &quot;TABLE_B&quot;.


Let's test it out:

const tableAlias1 = generateTableAlias(
   { alias: &#39;TABLE_A&#39;, property: &#39;hobbies&#39; } // error!
// ------------------&gt; ~~~~~~~~
// Type &#39;&quot;hobbies&quot;&#39; is not assignable to type &#39;&quot;name&quot; | &quot;id&quot;&#39;.
);

const tableAlias2 = generateTableAlias(
   { alias: &#39;TABLE_A&#39;, property: &#39;name&#39; }
); // okay

Looks good; this is the behavior you wanted.

Playground link to code

huangapple
  • 本文由 发表于 2023年2月16日 19:44:45
  • 转载请务必保留本文链接:https://go.coder-hub.com/75471780.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定