typescript:对象的键类型受同一对象的其他键约束

Typescript: Object with keys' types conditioned by other keys of the same object

好的,所以我有这个功能,应该从一个对象接收一个键和一个值。不同的键具有与之关联的不同的值类型。

我不想有一个通用的功能,例如:

1
2
3
function updateField(key: string, value: any) {

}

例如,我在这里有这个对象:

1
2
3
4
5
6
7
interface ISessionSpecific {
    students?: number[];
    subject?: number;
    address?: string;
    duration?: string[];
    date?: Date;
}

我想创建一个更新此对象中字段的函数,但我希望它正确键入...

所以当我打电话时:

1
2
const value = something;
updateField('address', value);

如果value不是字符串类型,则会抛出错误;

我尝试过的事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// First Approach:
type ForField<T extends keyof ISessionSpecific> = {
    field: T;
    value: Required<ISessionSpecific>[T];
};

type Approach1 =
    | ForField<'address'>
    | ForField<'students'>
    | ForField<'date'>
    | ForField<'duration'>
    | ForField<'subject'>;

// Second Approach
type Approach2<T extends ISessionSpecific = ISessionSpecific> = {
    field: keyof T;
    value: T[keyof T];
};

// Testing
const x: [Approach1, Approach2] = [
    { field: 'address', value: 0 }, // Error
    { field: 'address', value: 0 }, // No Error
];

我的第一种方法解决了我的问题,但我认为这太冗长了。因为我创建的此接口只是一个示例,所以实际的接口可能更大。
所以我想知道是否有任何更优雅的方法可以做到这一点


您可以利用分布条件类型来获取类似于自动生成的Approach1的类型:

1
2
3
type MapToFieldValue<T, K = keyof T> = K extends keyof T ? { field: K, value: T[K] } : never;

const foo: MapToFieldValue<ISessionSpecific> = { field: 'address', value: 0 } // Expect error;

MapToFieldValue<ISessionSpecific>的结果将等于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type ManualMap = {
    field:"students";
    value: number[] | undefined;
} | {
    field:"subject";
    value: number | undefined;
} | {
    field:"address";
    value: string | undefined;
} | {
    field:"duration";
    value: string[] | undefined;
} | {
    field: 'date';
    value: Date | undefined;
}

使用映射类型的另一种方法会产生相同的结果(感谢@Titian):

1
type MapToFieldValue< T > = { [K in keyof T]: { field: K, value: T[K] } }[keyof T]


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
interface ISessionSpecific {
    students?: number[];
    subject?: number;
    address?: string;
    duration?: string[];
    date?: Date;
}

function updateField<T, P extends keyof T>(obj: T, prop: P, value: T[P]) {
    obj[prop] = value;
}

var myObj: ISessionSpecific = {};


updateField(myObj,"date", new Date()); // OK!
updateField(myObj,"nonExistingProp","some Value"); // Error"nonExistingProp" is not a valid prop.

let subject1 = 10; // inferred type : number
let subject2 ="10"; // inferred type : string

updateField(myObj,"subject", subject1); // OK!
updateField(myObj,"subject", subject2); // Error Argument of type 'string' is not assignable to parameter of type 'number | undefined'.
updateField(myObj,"subject", undefined); // OK because ISessionSpecific has subject as optional

// if you want the 3rd paramater to be not null or undefined you need to do this:
function updateFieldNotUndefined<T, P extends keyof T>(obj: T, prop: P, value:  Exclude<T[P], null | undefined>) {
    obj[prop] = value;
}
updateFieldNotUndefined(myObj,"subject", undefined); // ERROR!


// If you want to pass an object of Key-Value pairs:
function updateFieldKeyValuePair<T, P extends keyof T>(
    obj: T,
    kvp: { prop: P, value:  Exclude<T[P], null | undefined> }
) {
    obj[kvp.prop] = kvp.value;
}


// if you want to put it in a class:
class SessionSpecific implements ISessionSpecific {
    students?: number[];
    subject?: number;
    address?: string;
    duration?: string[];
    date?: Date;

    public updateField<P extends keyof this>(prop: P, value: this[P]) {
        this[prop] = value;
    }
}

游乐场