I encountered a situation that has left me unsure whether it is an actual issue or simply a misunderstanding on my part.
Upon reviewing this code snippet:
type Props = {
foo: string
bar: string
} & Record<string, unknown>
// Using Record<string, unknown> seems to be the recommended way of saying "any object"
// Everything looks good, no errors: the known properties appear to be preserved in the intersection
const obj: Props = { foo: '', bar: '' }
type KeysOfProps = keyof Props // In this case, this type is "string"
These observations have led me to the following conclusions:
- If
keyof Props is string
, thenProps
should not allowobj
to havetoto
andtata
as strings. - If
{ foo: string; bar: string }
can be assigned toProps
, thenkeyof Props
should still include the known propertiesfoo
andbar
. (Otherwise,foo
andbar
should be unknown in my understanding)
Despite all this, I have discovered that keyof Props
is probably 'foo' | 'bar' | string
, which simplifies to string
.
However, this has led to some complications in my particular case. For instance:
// This results in Record<string, unknown>, causing 'bar' to be lost in the process
type PropsWithoutFoo = Omit<Props, 'foo'>
// This can be explained by how Omit is defined:
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// Since `keyof T` is `string` and K is 'foo': Exclude<string, 'foo'> is string
// Pick<Props, string> becomes 'foo' | 'bar' | unknown which simplifies to unknown
// => All keys of the intersection are lost
This issue arose in a codebase involving generics, where Record<string, unknown>
serves as a default type that can be substituted by any type that extends it. The loss of my properties appears to stem from this, hence the need to address this issue.