英文:
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 "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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论