通过使生成的函数也是泛型的,您可以基于T
在两个参数之间建立关系:
type UpdateFieldValue<T extends Record<string, unknown>> = <K extends keyof T>(key: K, value: T[K]) => void
declare const foo: UpdateFieldValue<{ age: number, name: string }>
// Works
foo('age', 3);
// No good
foo('name', 3);
TypeScript Playground个
这在第一个参数的类型已经缩小为文字类型的情况下非常有效,第一个参数的类型是TypeScrip用来推断泛型类型K
并通过它的T[K]
.
但是,如果您try 在泛型类型设置为联合的情况下调用它,无论是显式还是通过推理,则可能会遇到麻烦:
type UpdateFieldValue<T extends Record<string, unknown>> = <K extends keyof T>(key: K, value: T[K]) => void
declare const foo: UpdateFieldValue<{ age: number, name: string }>
// Works
foo<'age' | 'name'>('age', 'string');
const bar = Math.random() > 0.5 ? 'age' : 'name';
// Also works
foo(bar, 3);
TypeScript Playground个
如果这对您来说是个问题,我想我已经发现强制只允许字符串文字类型.但它需要使用一些复杂的实用程序类型来完成此任务.此外,对于某些类型(例如Record<string, boolean>
),无论如何这可能不是首选的行为.但为了完整起见,我想将其包括在内.
它使用Equals
类型和UnionToIntersection
类型将参数的类型转换为其联合成员的交集,然后判断它是否没有更改.这应该只适用于文本类型,它可以被视为具有1个成员的联合,而对于never
(实际上是具有0个成员的联合).
下面是该实现可能是什么样子:
/**
* @see {@link https://github.com/microsoft/TypeScript/issues/27024#issuecomment-421529650}
*/
type Equals<A, B> = (
<T>() => T extends A ? 1 : 2
) extends (
<T>() => T extends B ? 1 : 2
) ? true : false;
/**
* @see {@link https://stackoverflow.com/a/50375286/1710523}
*/
type UnionToIntersection<U> = (
U extends unknown ? (arg: U) => void : never
) extends (arg: infer I) => void ? I : never;
type LiteralOnly<S> = Equals<S, UnionToIntersection<S>> extends true ? S : never;
type UpdateFieldValue<T extends { [key: string]: unknown; }> = <K extends keyof T>(key: LiteralOnly<K>, value: T[K]) => void;
declare const foo: UpdateFieldValue<{ age: number, name: string }>;
// No good
foo<'age' | 'name'>('age', 'string');
foo('age', 'string');
// Works
foo('age', 3);
foo('name', 'string');
const uncertainKey = Math.random() > 0.5 ? 'age' : 'name';
// No good
foo(uncertainKey, 3);
declare const bar: UpdateFieldValue<Record<string, boolean>>;
// Still not allowed, but perhaps should be
bar(uncertainKey, true);
// Works
bar('foo', true);
TypeScript Playground个
另一种方法没有上面讨论的泛型解决方案中讨论的缺点,它是通过将参数类型定义为单个类型来链接它们.这可以使用扩展语法和元组类型来实现.
通过使用immediately indexed mapped type可以将该元组类型构造为区分并集,这应该允许您通过缩小函数中的任一参数类型来缩小函数中两个参数的类型.
这种方法的缺点是,IntelliSense不是很好,当传递无效参数时,不清楚出了什么问题.但如果你关心的是某件事是否被允许,这种方法似乎确实能带来更好的结果:
type UpdateFieldValue<T extends Record<string, unknown>> = (
...[key, value]: { [K in keyof T]: [K, T[K]] }[keyof T]
) => void
declare const foo: UpdateFieldValue<{ age: number, name: string }>
// No good
foo<'age' | 'name'>('age', 'string');
foo('age', 'string');
// Works
foo('age', 3);
foo('name', 'string');
const uncertainKey = Math.random() > 0.5 ? 'age' : 'name';
// No good
foo(uncertainKey, 3);
declare const bar: UpdateFieldValue<Record<string, boolean>>;
// Allowed
bar(uncertainKey, true);
// Works
bar('foo', true);
TypeScript Playground个