#Unexpected `unknown` when using indexed access on a generic
21 messages · Page 1 of 1 (latest)
Preview:```ts
type Test = {
foo?: string
}
type Foo<T extends Test> = {
test?: T
}
function foo<T extends Test = Record<string, never>>(
opts: Foo<T>
): T["foo"] {
return opts.test?.foo
}
const ex1 = foo({})
// ^?
const ex2 = foo({test: {}})
// ^?
const ex3 = foo({test: {foo: undefined}})
...```
since it apparently equals
NonNullable<unknown>rather thanRecord<string, never>
this is expected, though it might be more direct to sayNonNullable<unknown>is{}- it kinda acts like{} | null | undefined, kinda like howbooleanacts liketrue | false
there's no real "empty object" type - {} is a type that doesn't require anything other than being indexable, so any non-nullish value
Record<string, never> is almost never the type you want
in this case, Record<string, undefined> would describe the type better yeah
also, setting defaults on function generics kinda breaks inference
you probably don't want that either. Record<string, T> is kinda overused tbh
Well, in this case, it would accurately describe what foo() actually returns, no?
you generally don't want to describe stuff exactly, just their constraints
the behaviour here specific might be because of the optional?
Okay, so apart from the eager constraint with undefined or string, the unknown is way wider than the constraint could ever be
type Test = {
foo?: string | undefined
}
type X = {} extends Test ? true : false
// ^? - type X = true
there's some soundness gaps regarding optionals
if you hover over foo in the call, you can see that T is being inferred to {}
which, unfortunately, does extend Test
Hm, looks like that's not the only part. For example
Preview:```ts
type Test = {
foo?: string
bar: number
}
type Foo<T extends Test> = {
test?: T
}
function foo<T extends Test>(opts: Foo<T>): T["foo"] {
return opts.test?.foo
}
const ex1 = foo({})
// ^?
const ex2 = foo({test: {bar: 0}})
// ^?
const ex3 = foo({test: {foo: undefined, bar: 0}})
...```
even here, T['foo'] evaluates to unknown
well, same thing, really?
Preview:```ts
type Test = {
foo?: string
bar: number
}
type Foo<T extends Test> = {
test?: T
}
function foo<T extends Test>(
opts: Foo<T>
): T extends {foo: infer V} ? V : undefined {
return opts.test?.foo
}
const ex1 = foo({})
// ^?
const ex2 = foo({test: {bar: 0}})
...```