我正在尝试为实用程序函数编写类型定义:
function getter<T>(field1, field2?, field3?) {
if (field3 !== undefined) {
return (obj: T) => obj[field1][field2][field3];
} else if (field2 !== undefined) {
return (obj: T) => obj[field1][field2];
} else {
return (obj: T) => obj[field1];
}
}
在给定以下接口的情况下,这样:
interface Contact { phone: string; }
interface Author { name: string; contact: Contact; }
interface Book { id: number; author: Author; }
实例示例:
const lordOfTheRings: Book = { id: 1, author: { name: 'JRR Tolkien', contact: { phone: '222222' } } };
您可以使用getter<Book>('author', 'name')
获得一个函数,当使用参数lordOfTheRings
调用该函数时,该函数将返回JRR Tolkien
。
因此,上面的函数可以正常工作,但类型安全。我希望能够编写getter<Book>('id')
并愉快地进行编译,但是如果我要编写getter<Book>('banana')
,请给它一个类型错误,因为banana
不是{{ 1}}。我只希望它能深入到三个层次。
我一直在研究这个问题,并努力编写一个适用于所有边缘情况的类型定义。
我能做的是这样的:
Book
我可以这样使用:
interface Getter<T> {
<K extends keyof T>(field1: K): (obj: T) => T[K];
<K extends keyof T, L extends keyof T[K]>(field1: K, field2: L): (obj: T) => T[K][L];
<K extends keyof T, L extends keyof T[K], M extends keyof T[K][L]>(field1: K, field2: L, field3: M): (obj: T) => T[K][L][M];
}
const bookGetter: Getter<Book> = getter;
就编译器而言,这是一种完美的行为,但我希望能够将类型指定为getter的参数,而不必像这样定义getter的特定实例。
我尝试了此功能的签名:
bookGetter('id') // No error
bookGetter('title') // Error
bookGetter('author')(lordOfTheRings).name; // No error
bookGetter('author', 'name'); // No error
bookGetter('author', 'age'); // Error
bookGetter('author', 'contact')(lordOfTheRings).phone; // No error
bookGetter('author', 'contact')(lordOfTheRings).email; // Error
bookGetter('author', 'contact', 'phone'); // No error
bookGetter('author', 'contact', 'email'); // Error
最初看起来很有希望,但在规范上却失败了,
function getter<T,
K extends keyof T = keyof T,
L extends keyof T[K] = keyof T[K],
M extends keyof T[K][L] = keyof T[K][L]>(field1: K, field2?: L, field3?: M) {
请不要担心此功能的实用性-在这一点上,它已经成为类型规范中的一项明智的练习!我们可以破解吗?
Click here to go to an example TS playground with these in。
使用TypeScript 3.1.4。
答案 0 :(得分:5)
定义getter
的方式中的问题是Typescript不支持部分参数推断。因此,当您说getter<Book>
时,将不会推断其余类型参数,它们只会使用其默认值。这意味着T[K]
将是T
所有属性类型的并集,并且可能没有任何公共密钥,因此,您得到的关于never
的错误。
部分类型推断可能会在3.3中添加到Typescript中(按照此PR),但是即使那样,您也必须编写类似getter<Book, *>
的方法,为每种类型添加*
您要推断的参数。
现在可以使用的解决方案(并且IMO实际上读起来更好)是使用函数currying(即返回函数的函数)。使用这种方法,您将在第一个调用中指定目标类型,并在第二个调用中让推理处理其余部分:
function getter<T>() {
function makeGetter<K extends keyof T>(field1: K): (obj: T) => T[K];
function makeGetter<K extends keyof T, L extends keyof T[K]>(field1: K, field2: L): (obj: T) => T[K][L];
function makeGetter<K extends keyof T, L extends keyof T[K], M extends keyof T[K][L]>(field1: K, field2: L, field3: M): (obj: T) => T[K][L][M];
function makeGetter(field1: keyof any, field2?: keyof any, field3?: keyof any) {
if (field2 !== undefined && field3 !== undefined) {
return (obj: any) => obj[field1][field2][field3];
} else if (field2 !== undefined) {
return (obj: any) => obj[field1][field2];
} else {
return (obj: any) => obj[field1];
}
}
return makeGetter;
}
// ---- Example Usage
interface Contact { phone: string; }
interface Author { name: string; contact: Contact; }
interface Book { id: number; author: Author; }
const bookGetter: Getter<Book> = getter;
const lordOfTheRings: Book =
{ id: 1, author: { name: 'JRR Tolkien', contact: { phone: '222222' } } };
getter<Book>()('id')(lordOfTheRings).name;
getter<Book>()('bones');
getter<Book>()('id'); // No error - correct
getter<Book>()('title'); // Error - correct
getter<Book>()('author')(lordOfTheRings).name; //ok
getter<Book>()('author', 'name'); // ok
getter<Book>()('author', 'age'); //error
getter<Book>()('author', 'contact')(lordOfTheRings).phone; // ok
getter<Book>()('author', 'contact')(lordOfTheRings).email; //error
getter<Book>()('author', 'contact', 'phone'); // ok
getter<Book>()('author', 'contact', 'email'); //error