[ty] Add support for dict(...) calls in typed dict contexts (#22113)

## Summary

fixes https://github.com/astral-sh/ty/issues/2127
- handle `dict(...)` calls in TypedDict context with bidirectional
inference
- validate keys/values using the existing TypedDict constructor implem
- mdtest: add 1 positive test, 1 negative test for invalid coverage

## Test Plan

```sh
cargo clippy --workspace --all-targets --all-features -- -D warnings  # Rust linting
cargo test  # Rust testing
uvx pre-commit run --all-files --show-diff-on-failure  # Rust and Python formatting, Markdown and Python linting, etc.
```
fully green
This commit is contained in:
Hugo 2025-12-20 16:59:03 +01:00 committed by GitHub
parent f9a0e1e3f6
commit 3ec63b964c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 40 additions and 0 deletions

View File

@ -55,10 +55,13 @@ class TD(TypedDict):
d1 = {"x": 1}
d2: TD = {"x": 1}
d3: dict[str, int] = {"x": 1}
d4: TD = dict(x=1)
d5: TD = dict(x="1") # error: [invalid-argument-type]
reveal_type(d1) # revealed: dict[Unknown | str, Unknown | int]
reveal_type(d2) # revealed: TD
reveal_type(d3) # revealed: dict[str, int]
reveal_type(d4) # revealed: TD
def _() -> TD:
return {"x": 1}

View File

@ -8448,6 +8448,43 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
arguments,
} = call_expression;
// Fast-path dict(...) in TypedDict context: infer keyword values against fields,
// then validate and return the TypedDict type.
if let Some(tcx) = tcx.annotation
&& let Some(typed_dict) = tcx
.filter_union(self.db(), Type::is_typed_dict)
.as_typed_dict()
&& callable_type
.as_class_literal()
.is_some_and(|class_literal| class_literal.is_known(self.db(), KnownClass::Dict))
&& arguments.args.is_empty()
&& arguments
.keywords
.iter()
.all(|keyword| keyword.arg.is_some())
{
let items = typed_dict.items(self.db());
for keyword in &arguments.keywords {
if let Some(arg_name) = &keyword.arg {
let value_tcx = items
.get(arg_name.id.as_str())
.map(|field| TypeContext::new(Some(field.declared_ty)))
.unwrap_or_default();
self.infer_expression(&keyword.value, value_tcx);
}
}
validate_typed_dict_constructor(
&self.context,
typed_dict,
arguments,
func.as_ref().into(),
|expr| self.expression_type(expr),
);
return Type::TypedDict(typed_dict);
}
// We don't call `Type::try_call`, because we want to perform type inference on the
// arguments after matching them to parameters, but before checking that the argument types
// are assignable to any parameter annotations.