Reputation: 2523
I try to define a type which can assign any key in an object. Then, I found this is a good answer.
type AnyMap = Record<string, any>;
const obj: AnyMap = {};
const obj1: AnyMap = {
a: 'a',
b: 'b'
}
However, I found AnyMap
could also be assigned to an array and it doesn't show any errors.
const arr1: AnyMap = ['a', 'b'];
I just want AnyMap
to only define the type of object, not including array. So, I try to create another type.
type UnknownMap = Record<string, unknown>;
// obj2 is works fine
const obj2: UnknownMap = {
a: 'a',
b: 'b'
}
// obj3 is works fine
const obj3: UnknownMap = {
0: 'a',
1: 'b'
}
// arr2 throws an error
// Type 'string[]' is not assignable to type 'UnknownMap'. Index signature is missing in type 'string[]'.(2322)
const arr2: UnknownMap = ['a', 'b'];
The result of UnknownMap
seems good to me. It only define the type of object and throws an error of array. Finally, I have two question that I don't quite understand.
If I only want to define the type of object and doesn't want to include the type of array. Is it better to use UnknownMap
instead of AnyMap
?
Why const arr2: UnknownMap = ['a', 'b']
throws the error Type 'string[]' is not assignable to type 'UnknownMap'. Index signature is missing in type 'string[]'.(2322)
? I don't know why it throws an error just because I replace any
with unknown
in Record
. Also, I don't know why obj3
is works fine but arr2
throws an error.
I hope the link above can help to explain my question well. If there is any description that makes you confused, please let me know, thank you.
Upvotes: 2
Views: 1027
Reputation: 33091
Part 1
Why const arr2: UnknownMap = ['a', 'b'] throws the error Type 'string[]' is not assignable to type 'UnknownMap'. Index signature is missing in type 'string[]'.(2322) ? I don't know why it throws an error just because I replace any with unknown in Record. Also, I don't know why obj3 is works fine but arr2 throws an error.
In order to better understand this error, please switch to TypeScript 4.4.0 version in TS playground.
Since, there was error messages improvement.
I believe this error (from TS 4.3.*):
Type 'string[]' is not assignable to type 'UnknownMap'. Index signature is missing in type 'string[]'
Is not correct because string[]
has index signature.
See lib.es5.d.ts
. This is a part of Array type declaration:
// lib.es5.d.ts
interface Array<T> {
[n: number]: T
}
If you switch to TS 4.4.0 - beta
in TS playground, you will se this error:
Type 'string[]' is not assignable to type 'UnknownMap'.
Index signature for type 'string' is missing in type 'string[]'
Above error message makes sense.
In order to understand TS error message better, consider next example:
declare var arr: { [prop: number]: string };
declare var rec: { [prop: string]: unknown };
arr = rec; // error
rec = arr // ok
Error message:
Type '{ [prop: string]: unknown; }' is not assignable to type '{ [prop: number]: string; }'.
'string' and 'number' index signatures are incompatible.
Type 'unknown' is not assignable to type 'string'.(2322)
Hence, we can reduce our problem to this:
declare var str: string;
declare var unk: unknown;
str = unk;
unk = str
So, the main problem here, that Type 'unknown' is not assignable to type 'string'
, whether any
is assignable to type string
.
Let's go back to our main error message:
type AnyMap = Record<string, any>;
const obj1: AnyMap = {
a: 'a',
b: 'b'
}
const arr1: AnyMap = ['a', 'b'];
type UnknownMap = Record<string, unknown>;
const obj2: UnknownMap = {
a: 'a',
b: 'b'
}
const obj3: UnknownMap = {
0: 'a',
1: 'b'
}
const arr2: UnknownMap = ['a', 'b']; // error
Message:
Type 'string[]' is not assignable to type 'UnknownMap'.
Index signature for type 'string' is missing in type 'string[]'
Since Record<string, unknown>
is just an alias for
{
[x: string]: unknown;
}
and ['a', 'b']
infered to string[]
, or in other words to
{
[x: number]: string;
}
index signature for [x: number]: string;
is not assignable to [x: string]: unknown
, because Type 'unknown' is not assignable to type 'string'
Keep in mind, that x:string
as an index signature itself is assignable to x:number
and vie versa, because according to JS specification, all keys are evaluated to string
.
Part 2
If I only want to define the type of object and doesn't want to include the type of array. Is it better to use UnknownMap instead of AnyMap ?
In order to distinguish dictionary and array, you need to infer this type from somewhere. You can either create an utility type for such purpose of infer it from function arguments. Please keep in mind, there is no negation
operator in TypeScript.
Consider this example:
type Primitives =
| string
| number
| symbol
| boolean
| undefined
| bigint
| null // not sure how you want to treat this one :D
type AnObject<T = any> =
T extends Primitives | Array<any> ? never : T
{
type Test = AnObject<Record<number, 2>> // ok
type Test2 = AnObject<[]> // never
type Test3 = AnObject<2> // never
}
const onlyObj = <T,>(obj: AnObject<T>) => { }
onlyObj([]) // Error
onlyObj(2) // Error
onlyObj({}) // ok
Here, in my blog, you can find more examples/information regarding type negation in typescript.
Upvotes: 4