-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow inferring generic function types "as const" #38968
Comments
@mmkal using overloads you can make your function getMatcher <T extends string> (sample: T): <U> (value: U) => value is T & U
function getMatcher <T extends number> (sample: T): <U> (value: U) => value is T & U
function getMatcher <T> (sample: T): <U> (value: U) => value is T & U
function getMatcher <T> (sample: T) {
return function <U> (value: U): value is T & U {
if (sample && typeof sample === 'object' && typeof value === 'object') {
return Object.entries(sample).every(([k, v]) => k in value && getMatcher(v)((value as any)[k]))
}
return (sample as any) === value;
}
} |
@robbiespeed yes, but I'm interested in inferring objects with string/number properties as literals, not just string/number values. This solution helps, but doesn't cover the example in the OP: getMatcher({ type: 'comment' }) Here, the generic will be inferred as |
@mmkal my mistake I missed the part where it was about properties, doesn't help that I forgot I did just stumble across how to make it work for properties, I thought it would only restrict the type going into Edit Updatedtype ConstRecord <T> = {
[P in keyof T]:
T[P] extends string ?
string extends T[P] ? string : T[P] :
T[P] extends number ?
number extends T[P] ? number : T[P] :
T[P] extends boolean ?
boolean extends T[P] ? boolean : T[P] :
ConstRecord<T[P]>;
}
type Const <T> =
T extends string ? T :
T extends number ? T :
T extends boolean ? T :
ConstRecord<T>
function getMatcher <T> (sample: Const<T>): (value: unknown) => value is T {
return function (value: any): value is T {
if (sample && typeof sample === 'object' && typeof value === 'object') {
return Object.entries(sample).every(([k, v]) => k in value && getMatcher(v as any)(value[k]))
}
return (sample as any) === value;
}
}
type APIRequest =
| {type: 'comment'; payload: {sender: string; issueId: number; body: string}}
| {type: 'issue'; payload: {sender: string; id: number; title: string; body: string}}
const commentMatcher = getMatcher({
type: 'comment',
payload: {sender: 'abc'},
}) // infers all nested properties as literal
const fooMatcher = getMatcher('foo') // infers 'foo' as literal
const handleRequest = (request: APIRequest) => {
if (commentMatcher(request)) {
console.log('parent: ' + request.payload.issueId) // request is correctly inferred as a comment
}
} Oldtype ConstRecord <T> = {
[P in keyof T]:
T[P] extends string ?
string extends T[P] ? never : T[P] :
T[P] extends number ?
number extends T[P] ? never : T[P] :
T[P] extends boolean ?
boolean extends T[P] ? never : T[P] :
ConstRecord<T[P]>;
}
type ConstValue <B, T extends B> = B extends T ? never : T;
function getMatcher <T extends string> (sample: ConstValue<string, T>): <U> (value: U) => value is T & U
function getMatcher <T extends number> (sample: ConstValue<number, T>): <U> (value: U) => value is T & U
function getMatcher <T extends boolean> (sample: ConstValue<boolean, T>): <U> (value: U) => value is T & U
function getMatcher <T> (sample: ConstRecord<T>): <U> (value: U) => value is T & U
function getMatcher <T> (sample: ConstRecord<T>) {
return function <U> (value: U): value is T & U {
if (sample && typeof sample === 'object' && typeof value === 'object') {
return Object.entries(sample).every(([k, v]) => k in value && getMatcher(v as any)((value as any)[k]))
}
return (sample as any) === value;
}
}
const oMatcher = getMatcher({ foo: true } as const); // type infered as { foo: 'bar' }
const oMatcherB = getMatcher({ foo: true }); // type also infered as { foo: 'bar' }
const oMatcherC = getMatcher({ foo: { bar: true } }); // works on nested props too!
const b = Math.random() < 0.5 ? { foo: true } : 'bar';
const barMatcher = getMatcher('bar');
if (oMatcherB(b)) {
b; // type guarded to { foo: true }, although it's not properly reducing the type and has a full type of:
// ({
// foo: true;
// } & string) | ({
// foo: true;
// } & {
// foo: boolean;
// })
// It might be possible to fix this with some tweaking
}
else if (barMatcher(b)) {
b; // type correctly guarded to 'bar'
} |
Duplicate of or strongly related to #30680 |
Yep, |
Great news! Thank you for pointing that out @midzdotdev |
Search Terms
const generic inference nested
Suggestion
Let generic function signature specify that type parameters should be inferred
as const
Use Cases
Allow writing library functions that specify how they want their generic parameters to be inferred. Right now, this is only possible with generics like
<T extends string>(value: T) => ...
, which doesn't cover objects - only literals.Examples
Let's say you have a basic runtime type-matching function:
If I try to use this to make a string matcher, it doesn't keep track of literal types:
This makes sense as default behaviour, since
'foo'
was passed in as a string. And it can be fixed at the call site withgetMatch('foo' as const)
. But this only works for typescript programs, and if we're writing a library, javascript users of that library won't get useful type inference. And it's not really intuitive. Even typescript users of the library will need to know somehow that this trick is available.It could also be fixed by changing the function signature:
But
getMatcher
is now limited to only work with strings. This doesn't cover the case of deeply nested objects:As far as I know, there's no way to tweak the
extends
constraint forT
which makes this keep track of the literaltype: 'comment'
andsender: 'abc'
values. So this feature request is to allow us to effectively ask the compiler to addas const
to any inferred type parameters:(syntax is just an idea which reuses the
as const
pattern in the generic typedef)If we could do this, it'd make it easy to use the matcher to write useful type guards:
Checklist
My suggestion meets these guidelines:
The text was updated successfully, but these errors were encountered: