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


评论