通用 keyof 约束:必须是特定类型对象的键

4
我很不确定这是否可能,但我已经很接近了。
如果我有这个对象/形状:
export const initialState: State = {
    foods: {
        filter: '',
        someForm: {
            name: '',
            age: 2,
        },
        someFormWithoutAnAge: {
            name: '',
        },
    }
};

declare function pickAForm<R extends keyof State, S extends keyof State[R]>(key1: R, key2: S): void;

该函数的运行良好,我可以使用类型安全地调用 pickAForm("foods", "someForm"),如果我执行 pickAForm("foods","somePropertyThatDoesntExist") 将会报错。

然而,我想要增加额外的安全性,即只能选择具有特定形状的项目。例如:someForm 应该可以通过,但 someFormWithoutAnAge 应该失败,因为您选择的任何内容都必须具有一个年龄属性。可以像这样:

declare function pickAFormWithAge<R extends keyof State, S extends keyof State[R], T extends State[R][S] & {age: number}>(key1: R, key2: S): void;

但是我并不确定如何开始。简而言之:

pickAFormWithAge('foods', 'someForm') // Passes
pickAFormWithAge('foods', 'someFormWithoutAge') // Should fail, does not look like {age: number}
pickAFormWithAge('foods', 'blah') // Should fail, not a key

如果你能在问题中添加State的定义,会很有帮助。 - Nitzan Tomer
状态看起来就像初始状态。所以可以考虑使用 type State = typeof initialState - silviogutierrez
你说可以工作的那一部分在 playground 中无法工作,它会产生这个错误:类型“"someForm"”的参数不能赋给类型“never”的参数。 - Nitzan Tomer
奇怪,这在 VS Code 上运行得很好,并且使用 webpack 编译也没有问题。也许 playground 正在使用旧版本?但是在普通的 TypeScript 中可以正常工作。 - silviogutierrez
1
尝试将S参数指定为S extends keyof (State[R] & { age: number; }) - timocov
显示剩余2条评论
1个回答

3
我能做到的唯一方法是:
a. 约束数据结构以匹配字符串文字,而不是相反。
b. 将数据结构作为函数参数传递。
const state = {
    foods: {
        filter: '',
        someForm: {
            name: 'Some form',
            age: 2
        },
        someFormWithoutAnAge: {
            name: 'other form',
            priority: 10
        }
    }
};

interface HasAge { age: number }

// IMPLEMENTATION
function getForm<O extends {[P in P1]: {[P in P2]: HasAge}}, P1 extends string, P2 extends string>(o: O, p1: P1, p2: P2) {
    return (o[p1] as any)[p2] as any;
}

// USAGE
const form1 = getForm(state, 'foods', 'someForm'); // GOOD
const form2 = getForm(state, 'foods', 'someFormWithoutAnAge'); // ERROR
const form3 = getForm(state, 'foods', 'blah'); // ERROR

一种更简单且灵活的解决方案是使用常规代码。 pickAForm 接受一个函数而不是字符串字面量。
const form1 = pickAForm((state) => state.foods.someForm);
// ...or...
const form1 = pickAForm(() => initialState.foods.someForm);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接