Using read-only array of strings as source for type
Instead of doing
export interface IPerson {
gender: 'm' | 'f' | 'nb' // Hard-coded strings
}
We can do
const GENDERS = ['m', 'f', 'nb'] as const
export interface IPerson {
gender: typeof GENDERS[number]
}
Using Enum and user-defined discriminator
On the same idea as above, one can use Enum and utility functions expanded from Enums
TypeScript type Discriminator factory
Create a type based on the property of another
We can create a type from picking a property from an existing nested one.
Say we want gender
from IPerson, because tat type doesn't exist
const GENDERS = ['m', 'f', 'nb'] as const
export interface IPerson {
likesChocolate: boolean
gender: typeof GENDERS[number]
}
In another file we can want the same property Without re-creating it, say we're
OK that IEmployee
has completely separate properties ... but we want the type
from the gender
property
import type { IPerson } from '../elsewhere'
export type IPersonGender = IPerson['gender'] // Boom!
export const IEmployee {
gender: IPersonGender;
}
We're basically piggy-backing on TypeScript's Indexable Type, it's also described in an older issue about "bracket notation"
We can also reuse GENDERS for a validator too
const GENDERS = ['m', 'f', 'nb'] as const
export type IPersonGender = typeof GENDERS[number]
// But that function isn't telling what it does
export const isGender = (g) => new Set([...GENDERS]).has(g)
If we want to make this more useful, so we can use it in if blocks or get type hints.
Those are This is an "User-Defined" assertions.
User-defined type assertion functions
A type guard is some expression that performs a runtime check that guarantees the type in some scope
Source User-defined type guards and TypeScript handbook about user-defined type guards
const GENDERS = ['m', 'f', 'nb'] as const
export type IPersonGender = typeof GENDERS[number]
export type IGenderIsser = (gender: string) => gender is IPersonGender
// See https://2ality.com/2015/01/es6-set-operations.html
const gendersSet = new Set([...GENDERS])
export const isGender: IGenderIsser = (gender) => {
let out = gendersSet.has(gender)
// Because it is a "x is Foo", we have to return a boolean
// MUST return boolean
return out
}
// Or, if we're inlining the type
export const isGender2 = (gender: string): gender is IPersonGender => {
// TYPING => ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// FUNCTION DEFINITION => ^^^^^^^^^^^^ ^
// The Typing is OVERLAPING of the function definition.
let out = gendersSet.has(gender)
// Because it is a "x is Foo", we have to return a boolean
// MUST return boolean
return out
}
Alternatively, you can use TypeScript 3.7’s Assertion Functions
export type IGenderAsserter = (
gender: unknown,
) => asserts gender is IPersonGender
export const assertsIsGender: IGenderAsserter = (input) => {
const out = isGender(input)
if (!out) {
throw new TypeError(`Unexpected value "${input}"`)
}
// MUST return undefined
}
// Or, if we're inlining the type
export const assertsIsGender2: (
gender: unknown,
) => asserts gender is IPersonGender = (input) => {
// TYPING => ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// FUNCTION DEFINITION => ^
// The Typing is NOT OVERLAPING of the function definition.
// https://github.com/microsoft/TypeScript/issues/34523#issuecomment-700491122
const out = isGender(input)
if (!out) {
throw new TypeError(`Unexpected value "${input}"`)
}
// MUST return undefined
}
Because the mechanics is a bit different from returning a boolean (it returns
void), we can make it clear in the code that we're aren't sure it's there (hence
unknown
) and then add the assertion function.