TypeScript: map strongly-typed collection [duplicate] - javascript

This question already has answers here:
Is it possible to restrict TypeScript object to contain only properties defined by its class?
(9 answers)
Closed 4 years ago.
I have a strongly-typed collection as follows:
interface IUser {
id: number,
name: string
}
const users: IUser[] = [
{ id: 1, name: 'Bob' },
// ...
];
Then, I create a new collection using the map function:
const nextUsers: IUser[] = users.map((user: IUser) => ({
ID: 3, // wrong field name
name: 'Mike',
id: 3,
}));
As you can see, there is a field with a wrong name - ID. Well, the question is why does it work?))

This is a byproduct of how typescript checks for excess properties. A little bit of background:
Type A is generally assignable from type B if type B has at least all the properties of type A. So for example, no error is expected when performing this assignment:
let someObject = { id: 3, name: 'Mike', lastName: 'Bob' }
let user: { id: number, name: string } = someObject // Ok since someobject has all the properties of user
let userWithPass : { id: number, name: string, pass: string } = someObject // Error since someobject does not have pass
The only time Typescript will complain about excess properties is when we try to directly assign an object literal to some object that has a known type:
// Error excess property lastName
let user: { id: number, name: string } = { id: 3, name: 'Mike', lastName: 'Bob' }
Now in your case, typescript will first infer the type of the result to map to the type of the returned object literal, which is all valid, and then will check that this type is compatible with the IUser array which it is, so no error since we never directly tried to assign the object literal to something of type IUser.
We can ensure we get an error if we explicitly set the return type of the arrow function passed to map
const nextUsers: IUser[] = users.map((user: IUser) : IUser => ({
id: 3,
name: 'Mike',
ID: 152,
}));

An interface defines some properties an object must have, but it is not exhaustive; the object can have other properties as well. This is demonstrated in the docs here.

It works because the properties of the interface are defined. The interface does not prevent additional properties from being added.
However :
const nextUsers: IUser[] = users.map((user: IUser) => ({
ID: 3, // wrong field name
name: 'Mike',
}));
This results in an error from the TypeScript compiler.

It stills works but if you had a typescript linter you would have an ERROR because the returned array type is different then IUse[]

Related

Typescript error using a variable string to locate the value in an object

I am trying to use as a variable to locate a value in an object, basically console.log(myobj.name) but use a variable instead of name e.g.
const myProperty = name:string
console.log(myObj[myProperty])
full details below (including interfaces)
The code runs but I get the following error in VSCODE.
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Details'.
below is the code the very last line is the one where I get the typescript error (using strict types)
interface Details {
id:number,
name:string,
email:string
}
interface Items {
[key: string]: Details[],
}
const items: Items = {
"blackberry":[
{
id: 1,
name: 'John Doe',
email: 'john#doe.de'
},{
id: 2,
name: 'Brad',
email: 'lorem#ipsum.com',
}
],
"orange":[{
id: 4,
name: 'Barry',
email: 'john#doe.de'
}
]
}
const myName:string = "name"
const myIx:string = "orange"
// console.log(items[myIx])
console.log(items[myIx][0].name)
console.log(items[myIx][0][myName]) // code runs but TS error here in VScode
You should use the correct type for myName:
const myName: keyof Details = "name"
This also has the advantage, that you get a compile error when you have a typo, e.g. this will fail:
const myName: keyof Details = "Name"
Typescript sees a string passed with object[str] as being able to all kinds of values. You can set the myIx to an enum to force it to be one of the actually existing keys within the interface or the quick and dirty fix can be to cast it to any like this (items[myIx][0] as any)[myName]

Create an object from an Interface using variables (in Typescript)

I'm new to typescript, and I'm trying to create a new object for an Interface that I defined. I know that I can do something like this:
interface Person {
name: string;
age: number;
}
const me: Person = {
name: "John"
age: 28,
};
However, what if I'd like to declare the variables first, and then build the object based on the variables I declared like this:
interface Person {
name: string;
age: number;
}
let name: string = "albert"
let age: number = 28
const me: Person = {
name: name
age: age,
};
This gives me the following error "Type 'void' is not assignable to type 'string'." Any idea of what's going on here?

TypeScript: is there a way to do const assertions on the array return by Object.values?

Here is the live demo: https://codesandbox.io/s/vigorous-rgb-u860z?file=/src/index.ts
enum Keys {
One = "one",
Two = "two"
}
const a = Object.values(Keys).map((value, index) => ({
label: value,
id: String(index)
})) as const;
Here is the error message
A 'const' assertions can only be applied to references to enum
members, or string, number, boolean, array, or object literals.
I am not sure what I am missing here since Object.values indeed returns an array. Why can't I make const assertion on it
The reason you can't do this is because the left hand side X of a const assertion X as const has to be a literal value that the TypeScript compiler knows.
So I assume your end goal is that you want to have the full type of A. Without any special stuff you get this:
const a: {
label: Keys;
id: string;
}[]
Now if you want to get this:
const a: [
{
label: "one";
id: "0";
},
{
label: "two";
id: "1";
}
]
This isn't really possible since you're depending on Object.values iteration order. Which is defined by ES2015, but typescript types don't "know" about it.
With some simple fancy types you can get to:
type EnumEntry<T> = T extends any ? { label: T, id: string } : never;
const a: EnumEntry<Keys>[] = Object.values(Keys).map((value, index) => ({
label: value,
id: String(index)
}));
const a: ({
label: Keys.One;
id: string;
} | {
label: Keys.Two;
id: string;
})[]
But that's the best I think we can do without depending on how typescript orders the types.
It's not exactly a const assertion, but you could do something like:
as ReadonlyArray<{label: Keys, id: string}> instead of as const.
Playground link

typescript undefined on the entire interface

How to declare the entire MyCustomObject interface can fallback to an empty object?
interface MyCustomObject {
name: string,
age: number
}
Below case is safe, it has default property of name and age as fallback, but sometime if the obj is from other source like an api, it can be some other type like an empty {} or even an empty []
const obj: MyCustomObject = {
name: "",
age: 0
}
I tried this
interface MyCustomObject {
name: string,
age: number
} | {}
it doesn't work that way.
the most correct way is to create a union
type IUnionType = MyCustomObject | {};
other ways to handle this is to make each property optional
interface MyCustomObject {
name?: string,
age?: number
}
Or create an optional object property where a union is defined in the original interface.
interface MyCustomObject2 {
obj: MyCustomObject | {}
}
examples
I have one interface having some member variables
suppose my interface name is IUser and variable name is iUser.
I have to make it undefine but I m getting error that we cannot make iUser = undefine
so I did something like this.
let obj: any;
this.iUser = obj;
obj = undefined
and it works for me

Error: Type '{}' is missing the following properties from type

interface Person {
name: string;
surname: string;
}
let person1: Person = {};
person1.name = "name"
person1.surname = "surname"
When I declare person1 I get this error:
Type '{}' is missing the following properties from type Person
This is a better way:
let person1: Person = {name: '', surname: ''};
But if you want exactly empty object than you can hack it like this:
let person1: Person = {} as Person;
Update after comment:
Look at this unpredictableFunction:
const unpredictableFunction = (): string|number|string[] => {
return Math.random() > 0.5 ? 'string' : Math.random() > 0.5 ? 9999 : ['1', '2', '3']
};
It may return number or it may return string or it may return array of strings
const person: Person = {name: '', surname: ''};
person.name = unpredictableFunction (); // this is a case you are talking about
In this case you will see
Type 'string | number | string[]' is not assignable to type 'string'.
Answers are:
Look at your code and ensure that you assign only strings to a Person properties,
Or update interface to be ready to a different values:
interface Person {
name: string | number | string[];
surname: string;
}
You have defined an interface with two required properties. So when you define an object with the type of the Person interface you must define these properties right away like this:
let person: Person = {
name: '',
surname: ''
}
However if you believe these properties are not required but are rather optional you can change your interface to this:
interface Person {
name?: string;
surname?: string;
}
Using the ? syntax you mark the property as optional. The following code should then work:
let person: Person = {};
in Typescript 2.0 we can do this better
let person1 ! : Person;
this "!" is Non-null assertion operator
according to documentation
A new ! post-fix expression operator may be used to assert that its operand is non-null and non-undefined in contexts where the type checker is unable to conclude that fact. Specifically, the operation x! produces a value of the type of x with null and undefined excluded. Similar to type assertions of the forms x and x as T, the ! non-null assertion operator is simply removed in the emitted JavaScript code.

Categories