我需要將一個配置物件傳遞給一個函式,其中該is欄位定義了正在創建的物件的原型,該options欄位定義了第一個建構式引數的屬性,例如:
{
testClassA: {
is: ClassA, // class ClassA { constructor(options: {foo: string}) }
options: {
foo: 'bar'
}
},
testClassB: {
is: ClassB, // class ClassB extends ClassA { constructor(options: {bar: number}) }
options: {
bar: 1
}
}
}
這就是我想要實作惰性按需物件創建的方式。問題是該條目U extends typeof ClassA將is屬性的有效值ClassA僅限于該類,并且我希望任何擴展它的類都被接受。
這是沙箱的鏈接。
任何幫助,將不勝感激。
UPD:完整示例源代碼:
class ClassA {
foo: string
constructor(options: {foo: string}) {
this.foo = options.foo
}
}
class ClassB extends ClassA {
constructor(options: {bar: number}) {
super({foo: ''})
}
}
function create<U extends typeof ClassA, A extends {[k in string]: {is: U} & {options: ConstructorParameters<U>[0]} }> (config: {
classes: A
}) {}
create({
classes: {
testClassA: {
is: ClassA,
options: {
foo: 'bar'
}
},
testClassB: {
is: ClassB,
options: {
bar: 1
}
}
}
})
uj5u.com熱心網友回復:
如果您希望編譯器在呼叫函式時推斷泛型函式的型別引數,則應確保這些型別引數有良好的推斷站點。讓我們看一個好的推理站點的簡單案例:
declare function foo<T>(x: T): void;
// good inference site: ^^^^
foo(new Date()); // function foo<Date>(x: Date): void
在這里, 的型別x是 的推理站點T,因為x呼叫時傳入的值foo()可用于推斷T。這是一個非常好的推理站點:xis的型別T,并且您希望編譯器推斷T. 這意味著編譯器可以只使用 的型別x作為候選物件,T而無需執行任何更復雜的推理技巧。正如你所看到的,在foo(new Date()),T被推斷為Date。
在光譜的另一端,你有這個:
declare function bar<T>(x: string): void;
// no inference site: ^^^^^^^^^^^^^^^^^^
bar("oops"); // function bar<unknown>(x: string): void
根本沒有推理站點T。因此,要求bar("oops")不給任何編譯從中可以開始推斷T,所以推斷失敗,并回落到一個隱含的約束的unknown。
大多數通用函式介于這些極端之間:
declare function baz<T>(x: T | string): void;
// inference site: ^^^^^^^^^^^^^
baz(Math.random() < 0.5 ? 0 : "");
// function baz<number>(x: string | number): void
baz("hello");
// function baz<string>(x: string): void
這里的型別x是T | string,并給定一個T | string您希望編譯器推斷的型別值T。在第一次呼叫中,我們傳入了一個型別為 的值,string | number編譯器成功推斷number為 的型別T。所以它做了一些類似于Exclude<number | string, string>get 的事情number。但是在第二次呼叫中,我們傳入了一個 type 值,string編譯器將其推斷string為 forT而不是Exclude<string, string>or的型別never。這兩個推論都是“正確的”,因為它們有效:string | stringis just string,但是根據用例,您可能喜歡也可能不喜歡那里的不一致。
再舉一個例子,然后我就停下來:
declare function qux<T>(x: { [K in keyof T]: Array<T[K]> }): void;
// inference site ----> ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
qux({ a: [1, 2, 3], b: ["x", "y"], c: [true, false] });
// function qux<{ a: number; b: string; c: boolean;}>(
// x: { a: number[]; b: string[]; c: boolean[]; }
// ): void
在這里,我們有一個可能看起來像一個糟糕的推理站點:我們需要T從 的值中進行推斷,這是{[K in keyof T]: Array<T[K]>}一個映射型別,其中的每個屬性T都包含在 中Array<>。但令人驚訝的是,編譯器成功了!您傳入一個 type 值,{a: number[], b: string[], c: boolean[]}編譯器撤消映射并生成一個Tof {a: number, b: string, c: boolean}。這被稱為映射型別的推斷(抱歉,沒有非棄用的手冊鏈接),它僅有效,因為映射型別是同態的,這意味著我們正在映射某些物件型別的屬性(您可以通過存在in keyof)。編譯器非常擅長這樣做。
無論如何,如果您發現推理沒有按照您想要的方式發生,您可能需要通過從它無法推理的事物轉向它可以推理的事物來改進推理站點。這是一個有點手搖的建議,因為需要一些經驗才能知道什么可以和不能很好地推斷。如果所有其他方法都失敗了,請使您的型別引數與傳入函式引數的型別相同,然后計算您真正想要的型別。
讓我們看一下您create的 呼叫簽名版本:
function create<
U extends typeof ClassA,
A extends { [k: string]: { is: U } & { options: ConstructorParameters<U>[0] } }
>(config: { classes: A }) { }
立即跳出我的第一件事是,有沒有推斷網站的U。您可能認為 for 的約束A將是這樣的推理站點,但這不是它的作業原理。有關通用約束不是 TypeScript 中的推理站點的權威宣告,請參閱microsoft/TypeScript#44711上的此評論。
這意味著推斷 forU將始終失敗并回退到typeof ClassA,因此您的代碼等效于
function create<
A extends { [k: string]: { is: typeof ClassA } & { options: { foo: string } } }
>(config: { classes: A }) { }
The good news is that you have a very good inference site for A, but the bad news is that you don't want A to be constrained this way. You want each property of A to possibly have its own one-arg subclass contructor of ClassA, which has a possibly different constructor argument from {foo: string}. You do want to allow different contructor arguments, and you don't want to allow two different constructor types for the is and the options properties for any particular property of A.
So, instead of having both is and options inside the type parameter constraint, let's just have the type parameter be a straightforward mapping from key names to one-arg constructors of ClassA instances, and use a mapped type to represent the is and options stuff:
function create<A extends { [K in keyof A]: new (options: any) => ClassA }>(config: {
classes: { [K in keyof A]: { is: A[K], options: ConstructorParameters<A[K]>[0] } }
}) { }
Here the constraint is recursive since it is of the form A extends {[K in keyof A]: ...}. But all it means is that we do not care about the keys of A, and the compiler accepts it. The constraint's property type is new (options: any) => ClassA, which is less specific than typeof ClassA since we don't care about the constructor argument type (except that the constructor doesn't need more than one argument) and we don't care about any static properties of the constructor either.
用于映射的型別classes的屬性config取每個屬性A[K]的A,這是一些一引數的構造ClassA或者-一個亞型的情況下,并且將其映射到{ is: A[K], options: ConstructorParameters<A[K]>[0] }。所以is屬性就是建構式型別,options屬性是建構式的第一個(也是唯一必需的)建構式引數。
那么,它有效嗎?
create({
classes: {
testClassA: {
is: ClassA,
options: {
//^? (property) options: {foo: string;}
foo: 'bar'
}
},
testClassB: {
is: ClassB,
options: {
//^? (property) options: {bar: number;}
bar: 2
}
}
}
})
是的,看起來不錯。如果我們做錯了,讓我們確保它會生氣:
create({
classes: {
oops: {
is: ClassB, options: { foo: "bar" } // error!
// ------------------> ~~~~~~~~~~
// Type '{ foo: string; }' is not assignable
// to type '{ bar: number; }'
}
}
})
萬歲!
Playground 鏈接到代碼
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/372521.html
標籤:打字稿
