Skip to content

Commit db91ac7

Browse files
authored
[ty] allow any string Literal type expression as a key when constructing a TypedDict (#20792)
1 parent 75f3c0e commit db91ac7

File tree

2 files changed

+48
-7
lines changed

2 files changed

+48
-7
lines changed

crates/ty_python_semantic/resources/mdtest/typed_dict.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,46 @@ Methods that are available on `dict`s are also available on `TypedDict`s:
4646
bob.update(age=26)
4747
```
4848

49+
`TypedDict` keys do not have to be string literals, as long as they can be statically determined
50+
(inferred to be of type string `Literal`).
51+
52+
```py
53+
from typing import Literal, Final
54+
55+
NAME = "name"
56+
AGE = "age"
57+
58+
def non_literal() -> str:
59+
return "name"
60+
61+
def name_or_age() -> Literal["name", "age"]:
62+
return "name"
63+
64+
carol: Person = {NAME: "Carol", AGE: 20}
65+
66+
reveal_type(carol[NAME]) # revealed: str
67+
# error: [invalid-key] "TypedDict `Person` cannot be indexed with a key of type `str`"
68+
reveal_type(carol[non_literal()]) # revealed: Unknown
69+
reveal_type(carol[name_or_age()]) # revealed: str | int | None
70+
71+
FINAL_NAME: Final = "name"
72+
FINAL_AGE: Final = "age"
73+
74+
def _():
75+
carol: Person = {FINAL_NAME: "Carol", FINAL_AGE: 20}
76+
77+
CAPITALIZED_NAME = "Name"
78+
79+
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "Name" - did you mean "name"?"
80+
# error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor"
81+
dave: Person = {CAPITALIZED_NAME: "Dave", "age": 20}
82+
83+
def age() -> Literal["age"] | None:
84+
return "age"
85+
86+
eve: Person = {"na" + "me": "Eve", age() or "age": 20}
87+
```
88+
4989
The construction of a `TypedDict` is checked for type correctness:
5090

5191
```py

crates/ty_python_semantic/src/types/typed_dict.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -387,21 +387,22 @@ fn validate_from_keywords<'db, 'ast>(
387387

388388
/// Validates a `TypedDict` dictionary literal assignment,
389389
/// e.g. `person: Person = {"name": "Alice", "age": 30}`
390-
pub(super) fn validate_typed_dict_dict_literal<'db, 'ast>(
391-
context: &InferContext<'db, 'ast>,
390+
pub(super) fn validate_typed_dict_dict_literal<'db>(
391+
context: &InferContext<'db, '_>,
392392
typed_dict: TypedDictType<'db>,
393-
dict_expr: &'ast ast::ExprDict,
394-
error_node: AnyNodeRef<'ast>,
393+
dict_expr: &ast::ExprDict,
394+
error_node: AnyNodeRef,
395395
expression_type_fn: impl Fn(&ast::Expr) -> Type<'db>,
396-
) -> Result<OrderSet<&'ast str>, OrderSet<&'ast str>> {
396+
) -> Result<OrderSet<&'db str>, OrderSet<&'db str>> {
397397
let mut valid = true;
398398
let mut provided_keys = OrderSet::new();
399399

400400
// Validate each key-value pair in the dictionary literal
401401
for item in &dict_expr.items {
402402
if let Some(key_expr) = &item.key {
403-
if let ast::Expr::StringLiteral(key_literal) = key_expr {
404-
let key_str = key_literal.value.to_str();
403+
let key_ty = expression_type_fn(key_expr);
404+
if let Type::StringLiteral(key_str) = key_ty {
405+
let key_str = key_str.value(context.db());
405406
provided_keys.insert(key_str);
406407

407408
let value_type = expression_type_fn(&item.value);

0 commit comments

Comments
 (0)