In summary: Typescript is more lenient with wide property types like string
than with more specific property types like 'a' | 'b' | 'c'
.
To simplify, your Keys
type is essentially the built-in Record
type which is defined as:
type Record<K extends string | number | symbol, T> = { [P in K]: T; }
So, for simplicity's sake, let's use that instead.
But why does Typescript behave this way?
function foo<T extends string | symbol>() {
const foo: Record<string, string> = {} // no issue
const bar: Record<T, string> = {} // error
}
The reason lies in how Typescript handles keys that are either infinite or finite.
string
can be any string value, so it's not strictly tracked.
'a' | 'b' | 'c'
represents a finite set of specific strings.
Typescript doesn't enforce the presence of infinite keys because it can't. It allows any string to be used as a key because the type definition permits it.
However, this leniency can lead to issues such as:
const obj: Record<string, string> = { a: 'test' }
obj.b.toUpperCase() // no type error, but may cause runtime crash
A more suitable type would be:
const obj: Record<string, string | undefined> = { a: 'test' }
obj.b.toUpperCase() // type error
obj.b?.toUpperCase() // okay
By allowing the value type to be undefined
, we ensure that properties must have a value before being treated as a string
, reinstating type safety.
When the compiler can determine the keys involved, it enforces stricter typing:
const obj: Record<'a', string> = { a: 'test' }
obj.b.toUpperCase() // type error
With more information available, Typescript applies stronger type checking in these cases.
Regarding this code snippet:
const foo = <T extends string|symbol>()=>{
const a: Record<T, string> = {} // type error
return a
}
Typescript assumes that T
will likely be inferred as a finite subset of string | symbol
, leading to stricter type checking.
However, in your code, no properties are assigned, although the types suggest otherwise:
foo<{ a: number }>().a // number
Since the property is never assigned, you'll encounter undefined
at runtime, potentially causing issues elsewhere in your code.