11# NewType
22
3- Currently, ty doesn't support ` typing.NewType ` in type annotations.
4-
53## Valid forms
64
75``` py
@@ -12,13 +10,133 @@ X = GenericAlias(type, ())
1210A = NewType(" A" , int )
1311# TODO : typeshed for `typing.GenericAlias` uses `type` for the first argument. `NewType` should be special-cased
1412# to be compatible with `type`
15- # error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `type`, found `NewType`"
13+ # error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `type`, found `< NewType pseudo-class 'A'> `"
1614B = GenericAlias(A, ())
1715
1816def _ (
1917 a : A,
2018 b : B,
2119):
22- reveal_type(a) # revealed: @Todo(Support for `typing.NewType` instances in type expressions)
20+ reveal_type(a) # revealed: A
2321 reveal_type(b) # revealed: @Todo(Support for `typing.GenericAlias` instances in type expressions)
2422```
23+
24+ ## Subtyping
25+
26+ The basic purpose of ` NewType ` is that it acts like a subtype of its base, but not the exact same
27+ type (i.e. not an alias).
28+
29+ ``` py
30+ from typing_extensions import NewType
31+
32+ Foo = NewType(" Foo" , int )
33+ Bar = NewType(" Bar" , Foo)
34+
35+ Foo(42 )
36+ Foo(Foo(42 )) # allowed: `Foo` is a subtype of `int`.
37+ Foo(Bar(Foo(42 ))) # allowed: `Bar` is a subtype of `int`.
38+ Foo(True ) # allowed: `bool` is a subtype of `int`.
39+ Foo(" forty-two" ) # error: [invalid-argument-type] "Argument is incorrect: Expected `int`, found `Literal["forty-two"]`"
40+
41+ def f (_ : int ): ...
42+ def g (_ : Foo): ...
43+ def h (_ : Bar): ...
44+
45+ f(42 )
46+ f(Foo(42 ))
47+ f(Bar(Foo(42 )))
48+
49+ g(42 ) # error: [invalid-argument-type] "Argument to function `g` is incorrect: Expected `Foo`, found `Literal[42]`"
50+ g(Foo(42 ))
51+ g(Bar(Foo(42 )))
52+
53+ h(42 ) # error: [invalid-argument-type] "Argument to function `h` is incorrect: Expected `Bar`, found `Literal[42]`"
54+ h(Foo(42 )) # error: [invalid-argument-type] "Argument to function `h` is incorrect: Expected `Bar`, found `Foo`"
55+ h(Bar(Foo(42 )))
56+ ```
57+
58+ ## The name must be a string literal
59+
60+ ``` py
61+ from typing_extensions import NewType
62+
63+ def _ (name : str ) -> None :
64+ _ = NewType(name, int ) # error: [invalid-newtype] "The first argument to `NewType` must be a string literal"
65+ ```
66+
67+ However, the literal doesn't necessarily need to be inline, as long as we infer it:
68+
69+ ``` py
70+ name = " Foo"
71+ Foo = NewType(name, int ) # allowed
72+ ```
73+
74+ ## The second argument must be a class type or another newtype
75+
76+ Other typing constructs like ` Union ` are not allowed.
77+
78+ ``` py
79+ from typing_extensions import NewType
80+
81+ # error: [invalid-newtype] "invalid base for `typing.NewType`"
82+ Foo = NewType(" Foo" , int | str )
83+ # error: [invalid-newtype] "invalid base for `typing.NewType`"
84+ # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
85+ Foo = NewType(" Foo" , 42 )
86+ ```
87+
88+ ## Newtypes can be cyclic in various ways
89+
90+ Cyclic newtypes are kind of silly, but it's possible for the user to express them, and it's
91+ important that we don't go into infinite recursive loops and crash with a stack overflow. In fact,
92+ this is * why* base type evaluation is deferred; otherwise Salsa itself would crash.
93+
94+ ``` py
95+ from typing_extensions import NewType, reveal_type, cast
96+
97+ # Define a directly cyclic newtype.
98+ A = NewType(" A" , " A" )
99+ reveal_type(A) # revealed: <NewType pseudo-class 'A'>
100+
101+ # Typechecking still works. We can't construct an `A` "honestly", but we can `cast` into one.
102+ a: A
103+ a = 42 # error: [invalid-assignment] "Object of type `Literal[42]` is not assignable to `A`"
104+ a = A(42 ) # error: [invalid-argument-type] "Argument is incorrect: Expected `A`, found `Literal[42]`"
105+ a = cast(A, 42 )
106+ reveal_type(a) # revealed: A
107+
108+ # A newtype cycle might involve more than one step.
109+ B = NewType(" B" , " C" )
110+ C = NewType(" C" , " B" )
111+ reveal_type(B) # revealed: <NewType pseudo-class 'B'>
112+ reveal_type(C) # revealed: <NewType pseudo-class 'C'>
113+ b: B = cast(B, 42 )
114+ c: C = C(b)
115+ reveal_type(b) # revealed: B
116+ reveal_type(c) # revealed: C
117+ # Cyclic types behave in surprising ways. These assignments are legal, even though B and C aren't
118+ # the same type, because each of them is a subtype of the other.
119+ b = c
120+ c = b
121+
122+ # Another newtype could inherit from a cyclic one.
123+ D = NewType(" D" , C)
124+ reveal_type(D) # revealed: <NewType pseudo-class 'D'>
125+ d: D
126+ d = D(42 ) # error: [invalid-argument-type] "Argument is incorrect: Expected `C`, found `Literal[42]`"
127+ d = D(c)
128+ d = D(b) # Allowed, the same surprise as above. B and C are subtypes of each other.
129+ reveal_type(d) # revealed: D
130+ ```
131+
132+ Normal classes can't inherit from newtypes, but generic classes can be parametrized with them, so we
133+ also need to detect "ordinary" type cycles that happen to involve a newtype. (This turns out to be
134+ tricky in the implementation, see comments around ` NewType::normalized_impl ` and
135+ ` NewType::apply_type_mapping_impl ` .)
136+
137+ ``` py
138+ E = NewType(" E" , list[" E" ])
139+ reveal_type(E) # revealed: <NewType pseudo-class 'E'>
140+ e: E = E([])
141+ reveal_type(e) # revealed: E
142+ ```
0 commit comments