TypeScript type guard doesn't realize string union has been reduced - typescript

In the following code, I expect that TypeScript should be able to realize that at the line with the comment, the user object cannot have any other value for role expect for admin.
And it kinda does! If you paste this code into TypeScript Playground, you will notice that hovering over role at that point does indeed show just admin!
But still, an error is shown at the return DemoForAdmin line. And hovering over just user there, the popup showing the type shows the full union for the role field.
function Demo(user: { name: string; role: 'user' | 'admin'; } | null) {
if (user === null) {
return 'You have to be logged in.';
}
if (user.role !== 'admin') {
return 'You have to be an admin';
}
user.role; // Hover over role to see "admin"
return DemoForAdmin(user);
}
function DemoForAdmin(user: { name: string; role: 'admin'; }) {
return 'You are an admin';
}
Shouldn't TypeScript be able to realize it is safe (it is, right?) to pass the object to that method and should it be able to infer the correct narrowed string union in both cases of hovering over user as well as role, not just role?

The type guard only applies to the field and only narrows the field, the type guard will not impact the containing object type.
There is another construct that allows you to do this called discriminated union. The type has to be a union of object types with a discriminat of a litearal type. In this case role can be used. This should work
function Demo(user: { name: string; role: 'user' } | { name: string; role: 'admin'; } | null) {
if (user === null) {
return 'You have to be logged in.';
}
if (user.role !== 'admin') {
return 'You have to be an admin';
}
user.role; // Hover over role to see "admin"
return DemoForAdmin(user);
}
function DemoForAdmin(user: { name: string; role: 'admin'; }) {
return 'You are an admin';
}
You can read more about discriminated unions here

Related

TypeScript: How to create a type or interface that is an object that has one of n keys or is a string?

I need to create an interface that is either a string or an object with one of three keys.
Basically I have a function that depending on the error returns something:
export const determineError = (error: ServerAlerts): AlertError => {
if (typeof error !== "string") {
if (error.hasOwnProperty("non_field_errors")) {
return error.non_field_errors[0];
} else if (error.hasOwnProperty("detail")) {
return error.detail;
} else if (error.hasOwnProperty("email")) {
return error.email[0];
} else {
return UNKNOWN_ERROR;
}
} else {
return error;
}
};
Here are the types:
export type AlertError =
| "Unable to log in with provided credentials."
| "E-mail is not verified."
| "Password reset e-mail has been sent."
| "Verification e-mail sent."
| "A user is already registered with this e-mail address."
| "Facebook Log In is cancelled."
| string;
export interface ServerAlerts {
non_field_errors: [string];
detail: string;
email: [string];
}
But the way I designed ServerAlerts here does not work for me, since ServerAlerts can also be a string and if it has one of its keys, it only has one.
How would you design such a type or interface?
EDIT: I tried making the keys optional by giving them a question mark, but then my linter complains in the respective key's error return statement in determineError.
If I'm understanding you correctly, just declare the parameter as being either ServerAlerts or string:
export const determineError = (error: ServerAlerts|string): AlertError => {
// -----------------------------------------------^^^^^^^
In a comment you've said that all three of the ServerAlerts properties are optional, so you need to mark them as such with ?:
interface ServerAlerts {
non_field_errors?: [string];
detail?: string;
email?: [string];
}
However, that means that anything typed object will also work, because all the fields are optional. So if you do both of those things, you get:
determineError("foo"); // Works
determineError({ non_field_errors: ["x"] }); // Works
determineError({ detail: "x" }); // Works
determineError({ email: ["x"] }); // Works
determineError({}); // Works (because all fields are optional)
let nonLiteralServerAlerts: object;
nonLiteralServerAlerts = { foo: ["x"] };
determineError(nonLiteralServerAlerts); // Works (because all fields are optional)
determineError({ foo: ["x"] }); // Fails (correctly)
Playground example
Which suggests you might just use object in the parameter signature. If you want to require one of the three fields (which I believe would do away with that UNKNOWN_ERROR branch), you'd define three interfaces and make ServerAlerts a union of them:
interface ServerAlertsNonFieldErrors {
non_field_errors: [string];
}
interface ServerAlertsDetail {
detail: string;
}
interface ServerAlertsEmail {
email: [string];
}
type ServerAlerts = ServerAlertsNonFieldErrors | ServerAlertsDetail | ServerAlertsEmail;
Then you'd use type assertions when returning the specific field:
if (error.hasOwnProperty("non_field_errors")) {
return (error as ServerAlertsNonFieldErrors).non_field_errors[0];
// ------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If you do that, then you get:
determineError("foo"); // Works
determineError({ non_field_errors: ["x"] }); // Works
determineError({ detail: "x" }); // Works
determineError({ email: ["x"] }); // Works
determineError({}); // Fails (correctly)
let nonLiteralServerAlerts: object;
nonLiteralServerAlerts = { foo: ["x"] };
determineError(nonLiteralServerAlerts); // Fails (correctly)
determineError({ foo: ["x"] }); // Fails (correctly)
Playground Example

How can a parent class determine which type was passed to a method, based on the subclass?

I'm trying to create subclasses to handle permissions for different areas of an app. Each subclass has a different list of valid actions that they handle, along with various reasons we show if the action is not allowed. I'm trying to define a hasPermissions method on the parent class to check if a permission is enabled, but I want it to complain if I pass in a string that is not a valid action for the current instance.
I know I can handle this by defining hasPermissions on each subclass, i.e. hasPermissions (actionName: UserActions) { ... }, but I'm hoping to avoid this. Is there a way the parent class can determine which actions are allowed, based on the the current instance?
declare var _;
type TPermission<TAction, TReason> = {
action: TAction;
enabled: boolean;
reason?: TReason;
}
class Permissions {
constructor(protected permissions) {
}
hasPermission(actionName) {
let permission = _.find(this.permissions, {
action: actionName
});
return permission ? permission.enabled : false;
}
}
type AdminActions = 'Add User' | 'Delete User';
type AdminReasons = 'MaxUsersReached' | 'CantDeleteAnotherAdmin';
type TAdminPermission = TPermission<AdminActions, AdminReasons>;
class AdminPermissions extends Permissions {
protected permissions: TAdminPermission[];
constructor(permissions: TAdminPermission[]) {
super(permissions);
}
}
type UserActions = 'Subscribe' | 'Unsubscribe';
type UserReasons = 'AlreadySubscribed' | 'AlreadyUnsubscribed';
type TUserPermission = TPermission<UserActions, UserReasons>;
class UserPermissions extends Permissions {
protected permissions: TUserPermission[];
constructor(permissions: TUserPermission[]) {
super(permissions);
}
}
let permissions: TUserPermission[] = [
{
action: 'Subscribe',
enabled: true
}
];
let user = new UserPermissions(permissions);
user.hasPermission('Subscribe'); // Valid, should return true
user.hasPermission('Unsubscribe'); // Valid, should return false
user.hasPermission('Add User'); // Invalid permission for UserPermissions, should error
You can make the parent class generic:
class Permissions<T extends string> {
...
hasPermission(actionName: T) {
...
}
}
And then:
class AdminPermissions extends Permissions<AdminActions> { ... }
class UserPermissions extends Permissions<UserActions> { ... }
Then the compiler will complain about this:
user.hasPermission('Add User');
Saying:
Argument of type '"Add User"' is not assignable to parameter of type
'UserActions'
As you wanted.
(code in playground)

typescript - assigning types to an exported variable of multiple types

I'm using nrgx/store and following the example app I'm using classes to strongly type my actions like
export const ActionTypes ={
SET_ACTIVE_USER: type('[UserState] Set Active User'),
SET_USERLIST: type('[UserState] Set Userlist')
};
export class SetActiveUserAction implements Action {
type = ActionTypes.SET_ACTIVE_USER;
constructor(public payload:User){}
}
export class SetUserlistAction implements Action {
type = ActionTypes.SET_USERLIST;
constructor(public payload:User[]){}
}
export type Actions
= SetActiveUserAction
| SetUserlistAction
I'm having now a type problem in my reducer. Since the exported Actions can be of type User or User[], it doesn't let me assign userlist:User[] to it (i want to use array methods like forEach)
export function reducer(state = INITIAL_STATE, action: userAction.Actions): State {
switch (action.type) {
case userAction.ActionTypes.SET_USERLIST: {
let userlist:User[] = action.payload <-------Type Issue
return tassign(state, { userlist: action.payload })
}
default: {
return state
}
}
}
One way to handle this is to cast the property so TypeScript knows what type it should be.
let userlist:User[] = <User[]>action.payload;
If that doesn't work then you can cast to any and then to the type.
let userlist:User[] = <User[]><any>action.payload;
But that's not really using the type system to it's full potential, and isn't very nice code.
Rather than forcing the type to be a specific type using a cast, it'd be nice if we could tell the type system how to figure out what type it is.
So ... it sounds like you want Type Guards.
https://www.typescriptlang.org/docs/handbook/advanced-types.html
(Look for the section titled 'User-Defined Type Guards')
You can make a special function that tests if an object is of a certain type.
That function returns a bool, true for a match, false for no match.
When you test a variable using that function in your code TypeScript recognises the type test and changes the type of that variable to match the tested type.
So you end up with code like this:
if (isUserAction(action)) {
let user: IUser = action.Payload;
}
I couldn't get them working with the exact object layout you have, but I made a similar example with a few tweaks that allow the type guards to work.
enum ActionType { User = 1, Users = 2 }
interface IAction { Type: ActionType; }
interface IUser { Name: string; }
class Action<T> implements IAction {
public Type: ActionType;
public Payload: T;
constructor(type: ActionType, payload: T) {
this.Type = type;
this.Payload = payload;
}
}
class UserAction extends Action<IUser> {
constructor(payload?: IUser) {
super(ActionType.User, payload);
}
}
class UsersAction extends Action<IUser[]> {
constructor(payload?: IUser[]) {
super(ActionType.Users, payload);
}
}
function isUserAction(action: IAction): action is UserAction {
return action.Type === ActionType.User;
}
function isUsersAction(action: IAction): action is UsersAction {
return action.Type === ActionType.Users;
}
function processAction(action: IAction) {
if (isUserAction(action)) {
let user: IUser = action.Payload;
console.log(`user: ${user.Name}`);
} else if (isUsersAction(action)) {
let users: IUser[] = action.Payload;
for (let user of users) {
console.log(`users: ${user.Name}`);
}
}
}
processAction(new UserAction({ Name: "foo" }));
processAction(new UsersAction([{ Name: "bar" }, { Name: "baz" }]));
Code # TypeScript Playground
Output:
user: foo
users: bar
users: baz
The documentation is very good and has lots of detail to teach you about this features, and all the rest.
Another good source is basarat.gitbooks.io.

TypeScript guard for nullable type doesn't work

Why guard for nullable type doesn't work in this case?
interface Person { name?: string }
let alex = {} as Person
function welcome(person: { name: string }) {}
if (alex.name != null) {
s = alex.name // Works nicely, no error here,
// it knows that alex.name isn't null
welcome(alex) // Wrongly complains here
// that `alex.name` could be null
}
You need to enable strictNullCheck compiler option.
Regardless of whether or not alex.name is null, the types { name?: string } and { name: string } aren't compatible.
You can do this:
function hasName(person: Person): person is { name: string } {
return person.name != null;
}
if (hasName(alex)) {
welcome(alex);
}
Edit
It might be an overkill, but that's how type guards work.
You can of course just type assert it:
welcome(alex as { name: string });

Object with arbitrary properties as class property

I am just starting out with Typescript, and I cannot understand if it's possible to have a class property be an object, which would hold any arbitrary property in addition to properties declared in the class. For example, I defined here a name as a property of a Person, and then under properties you should be able to define any other arbitrary characteristics for the person, such as their height. It seems that assigning it goes okay, but trying to access it on line 12 throws an error saying:
Property 'height' does not exist on type 'Object'
Fair enough! I know there is no guarantee that a property named height would be under something that is just an object, but there should still be a way to do this.
Here is the code:
class Person {
public name: string;
public properties: Object;
constructor(name: string, other: Object) {
this.name = name;
this.properties = other;
}
}
let props: Object = { height: 200 };
var nick = new Person("Bob", props);
console.log(nick.properties.height);
And here is an alternative I've tried, which throws exactly the same error:
class Person {
public name: string;
public properties: Object;
constructor(name: string, other:{ height: number }) {
this.name = name;
this.properties = other;
}
}
var nick = new Person("Bob", { height: 200 });
console.log(nick.properties.height);
Another alternative with an interface I've just tried, that still doesn't work.
interface PersonProperties {
height: number;
}
class Person {
public name: string;
public properties: Object;
constructor(name: string, other: PersonProperties) {
this.name = name;
this.properties = other;
}
getHeight(): number {
return this.properties.height;
}
}
var properties: PersonProperties = { height: 200 };
var nick = new Person("Bob", properties);
document.write(nick.getHeight().toString());
Since the static type of Person#properties is simply Object the type checker doesn't keep any additional type information about it, that's why you get the compile errors. You can solve this issue 2 ways
The "dumb" way with any:
class Person {
constructor(public name: string, public other: any) {}
/* ... */
}
const p = new Person("doge", { wow : "such property" })
console.log(p.other.wow) // no error, also no type checking
any in ts basically "disables" typechecking, and lets you access any properties on a variable typed as any
The slightly smarter way with generics
class Person<PropType> {
constructor(public name: string, public other: PropType) {}
}
const p = new Person("doge", { wow : "such property" })
console.log(p.other.wow) // ok
console.log(p.other.amaze) // error
This way each person instance will have an associated properties type, so it's checked "compile" time if the property you trying to access is known by the compiler.
I'd recommend some reading on generics if this doesn't look familiar from other languages: https://www.typescriptlang.org/docs/handbook/generics.html
The error happens because you're defining public properties: Object; and Object really doesn't have a property height. Even though you declare correct type in the constructor with { height: number } property properties is still expected to be an Object.
You could do for example this:
type PropertiesObject = { height: number };
class Person {
public name: string;
public properties: PropertiesObject;
constructor(name: string, other: PropertiesObject) {
this.name = name;
this.properties = other;
}
}
let props = <PropertiesObject>{ height: 200 };
var nick = new Person("Bob", props);
console.log(nick.properties.height);
Using interface as you did is correct as well.
See live demo
Note that you can always use good old square brackets notation. This should compile successfully even when using just Object:
console.log(nick.properties['height']);
You can use type mapping. Create type that consists of all keys (Wrap) and class which should map the properties you want to work with (UserIdentifier) and than mix that in parameter of your function (constructor of User object in my example)
type Wrap<T> = {
[P in keyof T]?: T[P]
}
type UserIdentifier = {
userId: string;
}
class User {
constructor(user: Wrap<any> & UserIdentifier) {
this.user = user;
}
user: Wrap<any> & UserIdentifier;
userId(): string {
return this.user.userId;
}
}
//then can be called as
let u = new User({userId:"usr1", someOther:1, andOther:2, andSoOn:3});
//or just
let u2 = new User({userId:"usr1"});
//however this will throw an error because required userId property is not present
let u3 = new User({someOther:1});

Resources