I want an array that allow only for the value of 'john' and 'james', both are not required, I tried below interface:
interface User {
name: ['john' | 'james']
}
but it doesn't work for
const user: User = ['john', 'james']
it work only
const user: User = ['john']
const user: User = ['james']
I think I missed something
You have typed an array with exactly one element that can either be john or james. If you want to allow multiple elements, you will need to do:
interface User {
name: ('john' | 'james')[]
}
Now all the following are valid:
const u1: User = {
name: ['john']
};
const u2: User = {
name: ['james']
};
const u3: User = {
name: ['john', 'james']
};
If the use of an interface is not mandatory, you can take advantage of union types:
type User = 'john' | 'james';
const user1:User[] = ['john', 'james', 'jane'];
// ----------------------------------> ~~~~~~
// Type '"jane"' is not assignable to type 'User'.(2322)
const user2:User[] = ['john', 'james', 'james']; // No errors
Related
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?
I have the following Object type in Flow:
type Identity = {
name: string,
lastName: string
};
type ExtraInfo = {
favoriteColor: string,
age: number
};
type Person = {
a: Identity,
b?: ExtraInfo
};
Is there a way to define from Person a type without any maybe type?:
// final result, but I don't want to redeclare all the properties, I want to derive from the Person type
type PersonWithExtraInfo = {
a: Identity,
b: ExtraInfo
};
I thought that by applying this would work, but apparently, not. Maybe becuase it's not a maybe type what's defined, but an optional property:
type NonMaybeType = <Type>(Type) => $NonMaybeType<Type>;
export type PersonWithExtraInfo = $ObjMap<Person, NonMaybeType>;
What I'd like to do is to create an object which is a Record of a certain interface and have TypeScript be able to infer the keys based on what's in my object. I've tried a few things, none of which do exactly what I'm looking for.
interface Person {
firstName:string
}
const peopleObj = {
Jason: {
firstName: "Jason"
},
Tim: {
firstName: "Tim"
}
} as const;
console.log(peopleObj);
Here, if you look at peopleObj, TypeScript knows the exact keys because of the as const. The problem here is I'm not enforcing each object to be a Person. So I tried this next:
const peopleObj: Record<string, Person> = {
Jason: {
firstName: "Jason"
},
Tim: {
firstName: "Tim"
}
} as const;
Here, each object has to be a Person because of the Record defined, but TypeScript loses its ability to know all of the keys because now they are just string instead of the constants 'Jason' | 'Tim', and this is the crux of the issue. I know I could explicitly use 'Jason' | 'Tim' in place of my string type, but this is a fairly large object in real life and updating that type explicitly every time I add to it or remove from it is getting to be tedious.
Is there a way to have the best of both worlds, where I can have TypeScript infer the keys in the object just based solely on what's in the object? I have found a way, although it's not super clean and I feel like there's likely a better way:
interface Person {
firstName:string
}
type PeopleType<T extends string> = Record<T, Person>;
const peopleObj: Record<string, Person> = {
Jason: {
firstName: "Jason"
},
Tim: {
firstName: "Tim"
}
} as const;
const People:Record<keyof typeof peopleObj, Person> = peopleObj;
console.log(People.Jason);
Your third method doesn't actually work I believe - as you can access People.foo without error. This is because when you construct peopleObj as Record<string, Person>, its type is now that. You then do keyof Record<string, Person>, which evaluates to string.
The only way I'm aware of to achieve this is via using a function with generics. This allows you to apply a constraint on the input parameter, and then return the original type.
const createPeople = <T extends Record<string, Person>>(people: T) => people;
const myPeople = createPeople({
Jason: {
firstName: "Jason"
},
Tim: {
firstName: "Tim"
}
});
console.log(myPeople.Jason);
console.log(myPeople.foo); // error
You have a catch 22 situation otherwise - i.e - I want to enforce that my keys are of type Person, but I don't know what my keys are.
One other way that you may prefer - which is basically the same thing but without the function:
interface Person {
firstName:string
}
// Force evaluation of type to expanded form
type EvaluateType<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
type PeopleType<T extends Record<string, Person>> = EvaluateType<{[key in keyof T]: Person }>;
const peopleLookup = {
Jason: {
firstName: "Jason"
},
Tim: {
firstName: "Tim"
}
};
const people: PeopleType<typeof peopleLookup> = peopleLookup;
console.log(people.Jason);
console.log(people.foo);
I'm expanding on #mbdavis's answer because there seems to be an additional constraint, which is that the key for each Person in the record is that Person's firstName property. This allows us to use mapped types.
A PeopleRecord should be an object such that for every key name, the value is either a Person object with {firstName: name} or undefined;
type PeopleRecord = {
[K in string]?: Person & {firstName: K}
}
Unfortunately it doesn't work the way we want it to if we just apply this type to peopleObj, since it doesn't see the names as anything more specific than string. Maybe another user can figure out how to make it work, but I can't.
Like #mbdavis, I need to reassign the object to enforce that it matches a more specific constraint.
I use the mapped type ValidateRecord<T> which takes a type T and forces that every key on that type is a Person with that first name.
type ValidateRecord<T> = {
[P in keyof T]: Person & {firstName: P}
}
Now we can reassign your peopleObj, which should only work if the object is valid, and will throw errors otherwise.
const validatedObj: ValidateRecord<typeof peopleObj> = peopleObj;
But it's a lot cleaner to do this assertion through a function. Note that the function itself merely returns the input. You could do any other validation checking on the JS side of things here.
const asValid = <T extends {}>( record: T & ValidateRecord<T> ): ValidateRecord<T> => record;
const goodObj = {
Jason: {
firstName: "Jason"
},
Tim: {
firstName: "Tim"
}
} as const;
// should be ok
const myRecordGood = asValid(goodObj);
// should be a person with {firstName: "Tim"}
const A = myRecordGood["Tim"]
// should be a problem
const B = myRecordGood["Missing"]
const badObj = {
Jason: {
firstName: "Bob"
},
Tim: {
firstName: "John"
}
} as const;
// should be a problem
const myRecordBad = asValid(badObj);
Playground Link
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[]
I have this:
public getFriends() : IUser[] {
let friends: IUser[];
friends[0].Id = "test";
friends[0].Email = "asdasd";
return friends;
}
Could sound stupid, but why am I getting friends[0] undefined? What am I supposed to do if I don't have a class of type User in this case.
In this statement:
let friends: IUser[];
You are declaring friends and its type IUser[], you aren't initializing it yet, so friends's value is undefined at this point.
So, the first step is to initialize it:
let friends: IUser[] = [];
Now it is an array, you have to push some content into it. First you have to create a IUser:
// this is one (likely) way of creating a IUser instance, there may be others
let friend: IUser = {Id: "test", Email: "asdasd"};
And then add it to the array:
friends.push(friend);
So your final code would be:
public getFriends() : IUser[] {
let friends: IUser[] = [];
let friend: IUser = {Id: "test", Email: "asdasd"};
friends.push(friend);
return friends;
}