使用字符串变量限制函数输入类型

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

Constrain function input type using string variable

问题

I have a few simple types:

export const structureTypes = ["atom", "molecule"] as const
export type Structure = typeof structureTypes[number]
export const atomTypes = [
  "hydrogen",
  "oxygen",
  "argon",
] as const
export const moleculeTypes = [
  "water",
  "methane",
] as const
export type AtomType = typeof atomTypes[number]
export type MoleculeType = typeof moleculeTypes[number]

I would like to be able to use these to constrain the possible inputs of an object that holds the IDs of each type:

export type StructureIdCache = {
  [key in Structure as string]: {
    [key in AtomType | MoleculeType]: string
  }
}

This works, but allowing AtomType | MoleculeType on the 2nd level allows me to use "water" as a key when the 1st level key is "atom" which is invalid. How can I modify the types to allow input of only allowable names in the 2nd level of the object?

I have tried creating a conditional type:

export type StructureType<T> = T extends "atom" ? AtomType : MoleculeType

This works for creating specific types:

type AtomTypeNames = StructureType<"atom">
type MoleculeTypeNames = StructureType<"molecule">

But does not allow me to constrain the inputs for a function:

export const getTypeId = async (
  c: Context,
  type: Structure,
  name: StructureType<typeof type> // <-- this should restrict name inputs but does not
): Promise<string> => {
  // do something
}

How can I restrict the valid inputs for name, based on the value of the string passed to type?

英文:

I have a few simple types:

export const structureTypes = [&quot;atom&quot;, &quot;molecule&quot;] as const
export type Structure = typeof structureTypes[number]
export const atomTypes = [
  &quot;hydrogen&quot;,
  &quot;oxygen&quot;,
  &quot;argon&quot;,
] as const
export const moleculeTypes = [
  &quot;water&quot;,
  &quot;methane&quot;,
] as const
export type AtomType = typeof atomTypes[number]
export type MoleculeType = typeof moleculeTypes[number]

I would like to be able to use these to constrain the possible inputs of an object that holds the IDs of each type:

export type StructureIdCache = {
  [key in Structure as string]: {
    [key in AtomType | MoleculeType]: string
  }
}

This works, but allowing AtomType | MoleculeType on the 2nd level allows me to use &quot;water&quot; as a key when the 1st level key is &quot;atom&quot; which is invalid. How can I modify the types to allow input of only allowable names in the 2nd level of the object?

I have tried creating a conditional type:

export type StructureType&lt;T&gt; = T extends Atom ? AtomType : MoleculeType

This works for creating specific types:

type AtomTypeNames = StructureType&lt;Atom&gt;
type MoleculeTypeNames = StructureType&lt;Molecule&gt;

But does not allow me to constrain the inputs for a function:

export const getTypeId = async (
  c: Context,
  type: Structure,
  name: StructureType&lt;typeof type&gt; // &lt;-- this should restrict name inputs but does not
): Promise&lt;string&gt; =&gt; {
  // do something
}

How can I restrict the valid inputs for name, based on the value of the string passed to type?

答案1

得分: 1

以下是您要翻译的内容:

One way to proceed is to write out a single const-asserted object whose type captures the desired relationship between your Structure and associated AtomType/MoleculeType:

const structures = {
  atom: ["hydrogen", "oxygen", "argon"],
  molecule: ["water", "methane"]
} as const;

And use it to generate a mapped type representing this relationship:

type StructureType = { 
  [K in keyof typeof structures]: typeof structures[K][number] 
};
    
/* type StructureType = {
    readonly atom: "hydrogen" | "oxygen" | "argon";
    readonly molecule: "water" | "methane";
} */

And now, instead of trying to make your generic function depend on a conditional type, which is essentially opaque to the compiler and difficult for it to reason about, you make it depend on an indexed access type corresponding to a property lookup:

export const getTypeId = async <K extends keyof StructureType>(
  type: K,
  name: StructureType[K]
): Promise<string> => {
  return null!
}

So type is some key type K of StructureType (that is, either "atom" or "molecule"), and name is the corresponding property type StructureType[K] of StructureType (so, for example if K is "atom", then StructureType[K] is "hydrogen" | "oxygen" | "argon"). You're not necessarily actually looking up a property in an object, but the compiler can reason fairly well about generic object lookups, and by phrasing the operation in those terms it compiles without error.


Let's try it out:

getTypeId("atom", "hydrogen"); // okay
getTypeId("atom", "water"); // error
getTypeId("molecule", "water"); // okay

Looks good. In the first and second cases, K is inferred as "atom", and so "hydrogen" is acceptable but "water" is not. In the third case, K is inferred as "molecule", and so "water" is acceptable. So the relationship between type and name is enforced, at least for these use cases.

英文:

One way to proceed is to write out a single const-asserted object whose type captures the desired relationship between your Structure and associated AtomType/MoleculeType:

const structures = {
  atom: [&quot;hydrogen&quot;, &quot;oxygen&quot;, &quot;argon&quot;],
  molecule: [&quot;water&quot;, &quot;methane&quot;]
} as const;

And use it to generate a mapped type representing this relationship:

type StructureType = { 
  [K in keyof typeof structures]: typeof structures[K][number] 
};
    
/* type StructureType = {
    readonly atom: &quot;hydrogen&quot; | &quot;oxygen&quot; | &quot;argon&quot;;
    readonly molecule: &quot;water&quot; | &quot;methane&quot;;
} */

And now, instead of trying to make your generic function depend on a conditional type, which is essentially opaque to the compiler and difficult for it to reason about, you make it depend on an indexed access type corresponding to a property lookup:

export const getTypeId = async &lt;K extends keyof StructureType&gt;(
  type: K,
  name: StructureType[K]
): Promise&lt;string&gt; =&gt; {
  return null!
}

So type is some key type K of StructureType (that is, either &quot;atom&quot; or &quot;molecule&quot;), and name is the corresponding property type StructureType[K] of StructureType (so, for example if K is &quot;atom&quot;, then StructureType[K] is `"hydrogen" | "oxygen" | "argon"). You're not necessarily actually looking up a property in an object, but the compiler can reason fairly well about generic object lookups, and by phrasing the operation in those terms it compiles without error.


Let's try it out:

getTypeId(&quot;atom&quot;, &quot;hydrogen&quot;); // okay
getTypeId(&quot;atom&quot;, &quot;water&quot;); // error
getTypeId(&quot;molecule&quot;, &quot;water&quot;); // okay

Looks good. In the first and second cases, K is inferred as &quot;atom", and so &quot;hydrogen&quot; is acceptable but &quot;water&quot; is not. In the third case, K is inferred as &quot;molecule&quot;, and so &quot;water&quot; is acceptable. So the relationship between type and name is enforced, at least for these use cases.

(There are some situations where K is inferred as a union type, and then StructureType[K] will also be a union, and the relationship might not be enforced the way you want. But this sort of issue is usually not a big deal for most users of generic functions, so I won't digress into a discussion of how to deal with that here.)

Playground link to code

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

发表评论

匿名网友

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

确定