问题
I'm trying to define two types, which should look something like:
export type IQuery<P, U> = {
request: string;
params: (props: P, upsteam?: U) => object;
key: (props: P, upstream?: U) => string;
forceRequest: boolean;
depends?: QueryMap
}
export type QueryMap = {
[k: string]: IQuery
};
The constraints I'm trying to express are that params
and key
have the same types for their two arguments, and that a QueryMap is just a mapping from a string to an arbitrary IQuery
(doesn't matter what the types are). The compiler complains here because it wants a type to be specified for IQuery
, but the point is that each IQuery
in the map should be independently parameterized. Is there any way to express this in typescript?
Additionally, if possible, I'd like to get information/guarantees about the shape of the upstream QueryMap
s present in the IQuery
as I iterated through this tree.
回答1:
The simplest thing you can do is this:
export type QueryMap = {
[k: string]: IQuery<any, any>
};
It's not completely type-safe, but it is not too far off what you're trying to represent. If you don't want to lose type information for a value of type QueryMap
, allow the compiler to infer a narrower type and use a generic helper function to ensure it is a valid QueryMap
, like this:
const asQueryMap = <T extends QueryMap>(t: T) => t;
const queryMap = asQueryMap({
foo: {
request: "a",
params(p: string, u?: number) { return {} },
key(p: string, u?: number) { return "hey" },
forceRequest: true
}
});
The value queryMap.foo.params
is still known to be a method that accepts a string
and an optional number
, even though the type QueryMap['foo']['params']
isn't.
If you specify something not assignable to a QueryMap
you will get an error:
const bad = asQueryMap({
foo: {
request: "a",
params(p: string, u?: number) { return {} },
key(p: string, u?: number) { return "hey" },
forceRequest: true
},
bar: {
request: 123,
params(p: number, u?: string) {return {}},
key(p: number, u?: string) {return "nope"},
forceRequest: false
}
}); // error! bar.request is a number
The not-completely type-safe problem is shown here:
const notExactlySafe = asQueryMap({
baz: {
request: "a",
params(p: number, u?: string) { return {} },
key(p: string, u?: number) { return "hey" },
forceRequest: true
}
});
This is accepted, even though there's no consistent reasonable values of P
and U
that works here (which is what happens when you use any
). If you need to lock this down more, you can try to have TypeScript infer sets of P
and U
values from the value or warn you if it cannot, but it's not staightforward.
For completeness, here's how I'd do it... use conditional types to infer P
and U
for each element of your QueryMap
by inspecting the params
method, and then verify that the key
method matches it.
const asSaferQueryMap = <T extends QueryMap>(
t: T & { [K in keyof T]:
T[K]['params'] extends (p: infer P, u?: infer U) => any ? (
T[K] extends IQuery<P, U> ? T[K] : IQuery<P, U>
) : never
}
): T => t;
Now the following will still work:
const queryMap = asSaferQueryMap({
foo: {
request: "a",
params(p: string, u?: number) { return {} },
key(p: string, u?: number) { return "hey" },
forceRequest: true
}
});
while this will now be an error:
const notExactlySafe = asSaferQueryMap({
baz: {
request: "a",
params(p: number, u?: string) { return {} },
key(p: string, u?: number) { return "hey" },
forceRequest: true
}
}); // error, string is not assignable to number
This increases your type safety marginally at the expense of a fairly complicated bit of type juggling in the type of asSaferQueryMap()
, so I don't know that it's worth it. IQuery<any, any>
is probably good enough for most purposes.
Okay, hope that helps; good luck!
回答2:
You could use IQuery<any, any>
.
I'm not sure what you're hoping for in the second part of the question. TypeScript doesn't give you runtime type information. If you just want to have type variables to refer to as you manipulate a single IQuery
, you can pass an IQuery<any, any>
to a function myFunction<P, U>(iquery: IQuery<P, U>) { ... }
.
回答3:
The Solution
I removed from your types unrelevant information just for clarity. The solution boils-down to basically add 3 lines of code.
type Check<T> = QueryMap<T extends QueryMap<infer U> ? U : never>
export type IQuery<P, U, TQueryMap extends Check<TQueryMap>> = {
prop1: (param1: P, param2?: U) => number;
prop2: (param1: P, param2?: U) => string;
prop3?: TQueryMap
}
export type QueryMap<T> = {
[K in keyof T]: T[K]
};
// type constructors
const asQueryMap = <T>(x: QueryMap<T>) => x
const asQuery = <P, U, V extends QueryMap<any>>(x: IQuery<P, U, V>) => x
Considerations
All types are correctly infered by the compiler.
Important: If (and only if) you use the type constructors
(see above) to construct yours structures you can consider yourself totally statically type-safe.
Bellow are the test cases:
Test of no compile errors
// Ok -- No compile-time error and correctly infered !
const queryMap = asQueryMap({
a: asQuery({
prop1: (param1: string, param2?: number) => 10,
prop2: (param1: string, param2?: number) => "hello",
}),
b: asQuery({
prop1: (param1: string, param2?: string) => 10,
prop2: (param1: string, param2?: string) => "hello",
}),
c: asQuery({
prop1: (param1: Array<number>, param2?: number) => 10,
prop2: (param1: Array<number>, param2?: number) => "hello",
})
})
const query = asQuery({
prop1: (param1: string, param2?: number) => 10,
prop2: (param1: string, param2?: number) => "hello",
prop3: queryMap
})
Test of Compile-time errors
You can see bellow some compile-time errors beeing catched.
// Ok --> Compile Error: 'prop2' signature is wrong
const queryMap2 = asQueryMap({
a: asQuery({
prop1: (param1: Array<string>, param2?: number) => 10,
prop2: (param1: Array<number>, param2?: number) => "hello",
})
})
// Ok --> Compile Error: 'prop3' is not of type QueryMap<any>
const query2 = asQuery({
prop1: (param1: string, param2?: number) => 10,
prop2: (param1: string, param2?: number) => "hello",
prop3: 10 // <---- Error !
})
Thank you Cheers
来源:https://stackoverflow.com/questions/51547360/typescript-map-of-arbitrary-generics