[typescript Utility Types][Record][Pick] Type 'A' is not assignable to type 'B'
Typescript Utility Type
Learn about Record and Pick among the utility types provided by Typescript. and the difference between using Object.keys(..) and Object.entries(..).
Utility type Record
the following Typescript code throws a type error.
code#01export class Morphheme {
tokens: string[];
elapsed: number;
constructor(tokens: string[], elapsed: number) {
this.tokens = tokens;
this.elapsed = elapsed;
}
static fromDto(
dto: Record<keyof Morpheme, Morpheme[keyof Morpheme]>,
) {
return new Morpheme(dto.tokens, dto.elapsed); // error at "dto.tokens"
^^^^^^^^^^
}
}
Argument of type 'number | string[]' is not assignable to parameter of type 'string[]'. Type 'number' is not assignable to type 'string[]'
the intention of [code#01] was to define the same property name and type for object literals as for class Morpheme
.
here, the intent of same property name and type is
- a) property tokens should be of type
string[]
. - b) property elapsed should be of type
number
.
however, if you use the utility type Record
in [code#01], the meaning of a) and b) is not the same.
Record utility classRecord<keyof Morpheme, Morpheme[keyof Morpheme]>
- the properties need only be "tokens" and "elapsed" for
keyof T
. - each property can be of type "string[]" or "number" -
T[keyof T]
.
this means that in the following [code#02], ra, rb, and rc all pass type checking.
CODE#02
type AsDto = Record<keyof Morphem, MorphemeDto[keyof Morpheme]>;
const ra:AsDto = {
tokens: 23, // eh?
elapsed: 45
}
const rb:AsDto = {
tokens: ['b', 'k'],
elapsed: ['a'] // huh?
}
const rc:AsDto = {
tokens: 23, // Oh my...
elapsed: ['a'] // Oh no...
}
- tokens can be
number | string[]
. - elapsed can be
number | string[]
.
Utility type Pick
to limit the properties to a specific type (morphology) and its type, use the utility type Pick as follows.
CODE#03type AsPick = Pick<Morpheme, 'tokens' | 'elapsed'>
const pa:AsPick = {
tokens: 23,
^^^^^^ // Type 'number' is not assignable to type 'string[]'
elapsed: 45
}
const pb:AsPick = {
tokens: ['b', 'k'],
elapsed: ['a']
^^^^^^^ // Type 'string[]' is not assignable to type 'number'
}
const pc:AsPick = {
tokens: 23,
^^^^^^ // Type 'number' is not assignable to type 'string[]'
elapsed: ['a']
^^^^^^^ // Type 'string[]' is not assignable to type 'number'
}
the utility type Pick is equivalent to the type definition below.
CODE#04type Pick<T, K extends keyof T> = { [P in K]: T[P]; }
- From T, pick a set of properties whose keys are in the union K
the generic type T
corresponds to Morpheme
in [code#03].
k extends keyof T` means the properties of type T (tokens, elapsed).
and [P in K]
constrains the type to be T[P]
for each such property.
here P
corresponds to either "tokens" or "elapsed".
- for "tokens", Morpheme["tokens"], which means the type string[].
- morpheme["elapsed"] for "elapsed", i.e., type number.
- note that "tokens" and "elapsed" are not string values, but are themselves types.
Object.keys(...), Object.entries(...)
here's a point to keep in mind about Typescript's type checking.
we've defined a function handle
that converts the dto to a class, like this
CODE#05type AsPick = Pick<Morpheme, 'tokens' | 'elapsed'>
const handle = (dto: AsPick) => {
return new Morphheme(dto.tokens, dto.elapsed);
}
now, if we call the function handle
with the following object as an argument, will we get a type error?
CODE#06const dtoFromServer = {
tokens: ['one', 'two'],
elapsed: 300,
ngram: 2
};
handle(dtoFromServer)
- dtoFromServer is given the type
{tokens: string[], elapsed: number, ngram: number }
by type inference. - the dtoFromServer has an extra property
ngram
(extra property)
no type error is raised in [code#06].
however, if you pass it as an object literal like this, it shows a type error.
CODE#07handle({
tokens: ['one', 'two'],
elapsed: 300,
ngram: 2
^^^^^
// Object literal may only specify known properties
// 'ngram' does not exist in type 'AsPick'.
})
Due to the nature of the Javascript language, which is controlled by Typescript, [CODE#06] the variable dtoFromServer may contain additional properties. the most common cases are
CODE#08(javascript)const dto = { ... };
if( isDownloadable(data) ) {
dto.enableDownload = true;
}
return dto;
- suddenly we have an enableDownload property. - Object Pollution
[code#06] would realistically look like this
CODE#09
const dtoFromServer = await analyze("Some texts are... ");
Javascript is seriously dynamic, so you can think of type checking in Typescript in the following sense.
At the very least, let's have this.
the type Pick<Morpheme, 'tokens' | 'elapsed'>
means that the object passed as an argument to the function must contain at least
- "tokens": string[]
- "elapsed": number
means it should be there (or "at least this should be there")
there may be other properties inside the object, but as long as the given property (and method) exists, no runtime error will be thrown because of the type (handle) inside the function.
in the case of [CODE#07], the compiler can be assured that there is no object pollution because the object literal is created and passed directly to the function.
so, unlike [CODE#06] and [CODE#09], we have stronger type checking.
now we iterate over the properties using Object.keys
.
CODE#10
type AsPick = Pick<Morpheme, 'tokens' | 'elapsed'>
const handle = (dto: AsPick) => {
Object.keys(dto).forEach((key) => {
console.log(key, dto[key]);
^^^^^^^^
})
}
- i see this a lot.
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'AsPick'. No index signature with a parameter of type 'string' was found on type 'AsPick'.
You can't be sure that the array returned by Object.keys contains only "tokens" and "elapsed".
it could have been passed through a bunch of functions, like [CODE#08], and gotten a strange property somewhere along the way.
so we can't be sure that dto[key]
is necessarily of type string[]
or that it's of type number
.
in this case, we use the following trick
CODE#11const handle = (dto: AsPick) => {
Object.keys(dto).forEach((key) => {
console.log(key, dto[key as keyof Morpheme]);
})
}
- take the variable key to be of type "tokens" or "elapsed":
as keyof T
the type of key is string
, but is forced to be typed as "tokens" | "elapsed"
.
"Let's say yes. don't question it."
the same limitation exists for Object.entries
.
suppose we have a type that represents subjects and scores, like this
CODE#12type Score = {
math: number,
eng: number,
kor: number;
}
const sumOfScore = (score: Score) => {
const acc = {lectures: 0, total: 0 };
return Object.entries(score).reduce((acc, [lecture, score]) => {
acc.total += score;
acc.lectures++;
return acc;
},acc)
}
the function sumOfScore accumulates the number of subjects and the total score.
from the second argument of recuce, we know that score
is of type number (because all its properties are of type number)
however, the type checking would fail if we received the following values from the server
CODE#13const scoresFromServer = {
math: 43,
eng: 89,
kor: 76,
date: '2024-10-12'
}
// console.log(sumOfScore(scoresFromServer))
// prints { "lectures": 4, "total": "2082024-10-12" }
- one day, the server puts in a
date
property.