You can encode a PersonT<Int?> value to see its output, which is the expected input for decoding. My experiment shows it's {"age":18,"name":"test","extra":null}. Not sure why the difference.
Swift generics is not C++ style substitution. When the compiler synthesizes PersonT.init(decoder:), the only constraint it sees is T: Codable, there's just not enough local information to treat extra: T as optional.
You can use this example to understand the local and static aspects in Swift generics.
// this will always return false
func isOptional<T>(_ t: T) -> Bool { t == nil }
let a: Int = 3
let b: Int? = 4
isOptional(a)
isOptional(b)