1

Suppose I have this type defined in my app:

type PiiType = 'name' | 'address' | 'email';

I use this type around the application to enforce some strong typing. We may get information from the server that ostensibly represents PII, and need to check if the type of the PII is valid or not by this type definition.

A previous solution suggested on this issue suggested having a second array duplicating the valid values, and checking strings against the contents of that array:

type PiiType = 'name' | 'address' | 'email';

isValidPii(value: string): Boolean {
  const piiTypeValues = ['name', 'address', 'email'];
  return piiTypeValues.indexOf(potentialValue) !== -1;
}

This is undesirable for us as it requires defining the type twice, removing a single source of truth and potentially creating errors.

How can I check if a given value is valid according to this union type, without duplicating its definition?

For example, if there was an operator like isoftype, I could use it like this:

'name' isoftype PiiType;      // true
'hamburger' isoftype PiiType; // false
100 isoftype PiiType;         // false

... but this operator doesn't exist, so I'm not sure what we should be doing instead to check if a value would be valid for this type. instanceof exists but only checks against classes/interfaces, and typeof only returns the JavaScript type (e.g. string or number).

We're considering using enums instead to represent this typing, but I'd like to check if there's something we can do with a native type instead first.

doppelgreener
  • 4,809
  • 10
  • 46
  • 63

2 Answers2

4

While duplicating the union is not ideal we can use the compiler to validare that the union and the duplicated values are in sync. If you don't have control over the union this is the safest way to go (otherwise @Matt-McCutchen's solution is the better way to go)

We can take advantage of mapped types and excess object literal properties check to create a function that will take an object with the same keys as the union. The values of the object don't matter we will just use the literal type 0

type PiiType = 'name' | 'address' | 'email';

function isValidBuilder<T extends string>(o: Record<T, 0>) {
  let values = Object.keys(o)
  return function (v: string): v is T {
    return values.indexOf(v) !== -1
  }
}

const isValidPii = isValidBuilder<PiiType>({
  name: 0,
  address: 0,
  email:0
})

// Error  missing key
const isValidPii2 = isValidBuilder<PiiType>({
  name: 0,
  address: 0,

})

//error excess key
const isValidPii4 = isValidBuilder<PiiType>({
  name: 0,
  address: 0,
  email: 0
  other:0
})
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • Post Typescript 3.4 there is an easier solution. See https://stackoverflow.com/questions/45251664/typescript-derive-union-type-from-tuple-array-values/45257357#45257357 – jtschoonhoven Apr 09 '20 at 23:28
3

The trick is to run the array through an identity function that infers an element type constrained by string. That will cause the compiler to infer a union of literal types:

function asLiterals<T extends string>(arr: T[]): T[] { return arr; }
const piiTypeValues = asLiterals(['name', 'address', 'email']);
type PiiType = (typeof piiTypeValues)[number];

There's another solution here but the above seems a bit simpler.

Matt McCutchen
  • 28,856
  • 2
  • 68
  • 75
  • 1
    This answer predates Typescript 3.4 and there is now an easier solution at the link provided in this answer. https://stackoverflow.com/questions/45251664/typescript-derive-union-type-from-tuple-array-values/45257357#45257357 – jtschoonhoven Apr 09 '20 at 23:26