I can't figure out why this code is throwing an error. - Type '{}' is not assignable to type 'Keys<T>'.
type Keys<T extends string|symbol>={
[key in T]: string;
};
const foo = <T extends string|symbol>()=>{
const a:Keys<T> = {}
return a
}
Moreover, if you manually substitute the string or symbol type, no errors will be received. Just a warning that T is declared for a function but not used.
Example of working codes:
type Keys<T extends string|symbol>={
[key in T]: string;
};
const foo = <T extends string|symbol>()=>{
const a:Keys<string> = {}
return a
}
type Keys<T extends string|symbol>={
[key in T]: string;
};
const foo = <T extends string|symbol>()=>{
const a:Keys<symbol> = {}
return a
}
You can check the code here
I expected generic code to work fine
CodePudding user response:
Because
const foo = <T = "a" | "b">()=>{
const a: {a: string, b: string} = {} // <- incompatible
return a
}
CodePudding user response:
TLDR: Typescript treats wide property types like string
much more leniently than property types like 'a' | 'b' | 'c'
.
First of all, your Keys
type is just the built in Record
type which is defined as:
type Record<K extends string | number | symbol, T> = { [P in K]: T; }
So I'm going to use that instead for simplicity.
So why is this the case?
function foo<T extends string | symbol>() {
const foo: Record<string, string> = {} // fine
const bar: Record<T, string> = {} // error
}
The answer is because Typescript behaves differently when a key type is infinite, versus when the key type is finite.
string
could be any string, which string is not tracked.'a' | 'b' | 'c'
is a finite list of strings.
Typescript doesn't bother enforcing the existence of infinite keys because it can't. The type says that any string returns a value, Typescript lets you use it like that.
And this does cause some problems:
const obj: Record<string, string> = { a: 'test' }
obj.b.toUpperCase() // no type error, crash at runtime
A better type for that would be:
const obj: Record<string, string | undefined> = { a: 'test' }
obj.b.toUpperCase() // type error
obj.b?.toUpperCase() // fine
Here the value type might be undefined
which means we would have to make sure each property has a value before we treat it as a string
. This gives us type safety back.
But when the compiler can know the keys, then it can enforce those keys, and the type checking gets much stricter:
const obj: Record<'a', string> = { a: 'test' }
obj.b.toUpperCase() // type error
Since this now has enough information to apply stronger types, it does.
So what's going here:
const foo = <T extends string|symbol>()=>{
const a: Record<T, string> = {} // type error
return a
}
Is that Typescript thinks that T
is probably going to be inferred as a finite subset of string | symbol
, and not the infinite wider type of string | symbol
. So it applies the stricter type checking.
And Typescript is right. Your code does not assign any properties at all, but the types says that this should work:
foo<{ a: number }>().a // number
But your code doesn't ever assign that property so you will get undefined
at runtime and that will probably crash something else.