1

I'd like to get an error if two types don't match. I have an object:

const ACTIVITY_ATTRIBUTES = {
    onsite: {
        id:  "applied",
        ....
    },
    online: {
        id: "applied_online",
        ....
    },
    ...
} as const

I'd like it to be limited to the strings the server can accept

export type ContactAttribute = "applied" | "applied_online" | "donated" | "messaged" | "reviewed"

I know as const can't go with a type limitation (as const will be ignored). But is there a way to to check for type equality, to enforce that the id property is of type ContactAttribute? Something like:

$Values<typeof ACTIVITY_ATTRIBUTES >["id"] === ContactAttribute
Ben Carp
  • 24,214
  • 9
  • 60
  • 72

5 Answers5

2

I'd come up with a helper function that only accepts arguments whose properties have an id property of type ContactAttribte, and returns its argument untouched, and without changing its type:

const hasGoodContactAttributes =
    <T extends Record<keyof T, { id: ContactAttribute }>>(t: T) => t;

Then you'd create ACTIVITY_ATTRIBUTES like this:

const ACTIVITY_ATTRIBUTES = hasGoodContactAttributes({
    onsite: {
        id: "applied",
        otherProp: 123,
    },
    donation: {
        id: "donated",
        alsoOtherProp: 456
        //....
    },
    online: {
        id: "applied_online",
        //....
    },
    reviewe: {
        id: "reviewed",
        //....
    },
}); // as const if you want

If you make a mistake, you'll get an error:

const BAD_ATTRIBUTES = hasGoodContactAttributes({
    okay: {
        id: "messaged"
    },
    oops: {
        id: "reviewed_online" // error!
    //  ~~ <-- '"reviewed_online"' is not assignable to type 'ContactAttribute'
    }
})

Okay, hope that helps; good luck!

Link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
2

This could be achieved using a dummy validating function.

const validateType = <T> (obj:T) => undefined 

All that is left is to call it with the type and object:

type ContactAttribute = "applied" | "applied_online" | "donated" | "messaged" | "reviewed"

type ActivityAttributes= {
   [k:string]: {
      id: ContactAttribute 
   }
}

const ACTIVITY_ATTRIBUTES = {
    onsite: {
        id:  "applied",
        ....
    },
    donation: {
        id: "donated",
        ....
    },
    ...
} as const

validateType<ActivityAttributes>(ACTIVITY_ATTRIBUTES) // Will show an error if types don't match. 
Ben Carp
  • 24,214
  • 9
  • 60
  • 72
1

You should check out this StackOverflow thread to know a easier way to achieve this.

Something like:

const ACTIVITY_ATTRIBUTES = {
    onsite: {
        id:  "applied",
    },
    donation: {
        id: "donated",
    },
    online: {
        id: "applied_online",
    },
    reviewe: {
        id: "reviewed",
    },
}

export type ContactAttribute = "applied" | "applied_online" | "donated" | "messaged" | "reviewed"

function isOfTypeContact (inputId: string): inputId is ContactAttribute {
    return ["applied", "applied_online", "donated", "messaged", "reviewed"].includes(inputId);
}

console.log(isOfTypeContact(ACTIVITY_ATTRIBUTES.onsite.id)) // true

Should work.

KingDarBoja
  • 1,033
  • 6
  • 12
1

You can achieve this by using a combination of declare and dead code. The declare statements are purely compile time so we use those to set up fake "values" of our types. Then we use a dead code block to call a function that takes two parameters of the same type.

type T1 = number;
type T2 = string;

declare const dummy1: T1;
declare const dummy2: T2;
declare function sameType<T>(v1: T, v2: T): void;
if (false) {
    // @ts-ignore: Unreachable code
    sameType(
        // prevent line break so that the ts-ignore is only applied to the above line
        dummy1, dummy2);
}

The @ts-ignore is to silence the unreachable code error.

I added a line comment after ( to prevent tools like prettier from putting the arguments on the same line which would silence the error you want to achieve.

You can also use an as expression which might be a little bit more concise.

type T1 = number;
type T2 = string;

declare const dummy1: T1;
if (false) {
    // @ts-ignore: Unreachable code
    dummy1 
        // prevent line break so that the ts-ignore is only applied to the above line
        as T2
}
Erik Arvidsson
  • 915
  • 9
  • 10
0

You can actually achieve that by using the following code:

const ACTIVITY_ATTRIBUTES: {[key: string]: {id: ContactAttribute}} = {
    onsite: {
        id:  "applied",
    },
    donation: {
        id: "donated",
    },
    online: {
        id: "applied_online",
    },
    reviewe: {
        id: "reviewed",
    },
    hi: {
      id: 'applied',
    }
} as const

type ContactAttribute = "applied" | "applied_online" | "donated" | "messaged" | "reviewed"

However, I suspect this might want more typechecking than this, because with the solution above, even the id can be checked properly, you still can have falsy property placed for different id.

To avoid this issue, you might want to use discriminated union.

For example:

type Contact = {
  id: "applied",
  x: string
} | {
  id: "applied_online"
  y: number
}

By leveraging this feature, you can make sure that the expected properties associated with each ContactAttribute would be initialised correctly.

Wong Jia Hau
  • 2,639
  • 2
  • 18
  • 30