As developers, we strive to write robust and error-free code. One of the ways to achieve this is by leveraging TypeScript's powerful type inference system.
This code snippet demonstrates how we can infer TypeScript types from JavaScript variables using the as const
syntax.
By using the as const
syntax, we can tell TypeScript that the variable's values are constant and should not be mutated.
This allows TypeScript to infer literal types for the values, making our code more type-safe and reducing the likelihood of runtime errors.
Furthermore, the code snippet also shows how we can use custom type guards to check types at runtime. By defining our own type guards, we can ensure that the values we receive conform to the expected types. This not only helps us catch errors early on but also makes our code more resilient to changes in the data structures.
Overall, leveraging TypeScript's type inference and custom type guards can significantly improve the quality and robustness of our code. By catching errors at compile-time and runtime, we can deliver more reliable software that provides a better experience for our users.
// ============ Data ============
const CANIDAE = ['dog', 'wolf', 'fox', 'coyote'] as const;
const FELINAE = ['cat', 'lion', 'tiger', 'lynx'] as const;
const REPTILIA = ['snake', 'turtle', 'lizard', 'gecko'] as const;
const ANIMALS = {
canidae: CANIDAE,
felinae: FELINAE,
reptilia: REPTILIA,
};
// ============ Inferred Types ============
type Canis = typeof CANIDAE[number];
type Felis = typeof FELINAE[number];
type Reptil = typeof REPTILIA[number];
type Animal = Canis | Felis | Reptil; // typeof ANIMALS[keyof typeof ANIMALS][number];
type Animals = typeof ANIMALS;
type AllowedAnimal = 'dog' | 'cat'; // typeof CANIDAE[0] | typeof FELINAE[0];
type AllowedAnimals = DeepPick<Animals, AllowedAnimal>;
// ============ Type Guards ============
const isCanis = (animal: Animal): animal is Canis =>
CANIDAE.some((canis) => canis === animal);
const isFelis = (animal: Animal): animal is Felis =>
FELINAE.some((felis) => felis === animal);
const isReptil = (animal: Animal): animal is Reptil =>
REPTILIA.some((reptil) => reptil === animal);
const isAllowedAnimal = (animal: Animal): animal is AllowedAnimal =>
animal === 'dog' || animal === 'cat';
// ============ Functions ============
const getAllowedAnimalsObj = (animals: Animal[]): AllowedAnimals => {
return animals.reduce((allowedAnimals: AllowedAnimals, animal: Animal) => {
if (isCanis(animal) && isAllowedAnimal(animal)) {
allowedAnimals.canidae = [animal];
}
if (isFelis(animal) && isAllowedAnimal(animal)) {
allowedAnimals.felinae = [animal];
}
return allowedAnimals;
}, {} as AllowedAnimals);
};
const getAllowedAnimalsArr = (animals: Animals): AllowedAnimal[] =>
Object.values(animals)
.concat()
.flat(1)
.filter((animal: Animal): animal is AllowedAnimal => isAllowedAnimal(animal));
// ============ Logs ============
console.log(getAllowedAnimalsObj([...CANIDAE, ...FELINAE, ...REPTILIA]));
console.log(getAllowedAnimalsArr(ANIMALS));
// ============ Utils ============
type DeepPick<
T extends Record<string, readonly unknown[]>,
O extends T[keyof T][number]
> = { [P in keyof T]: O extends T[P][number] ? [O] : never };
The as const
syntax tells TypeScript that the values of the array or object are immutable (marked as readonly
), allowing TypeScript to infer a more specific type. In the code example provided, the as const
syntax is used to define three arrays of animals: CANIDAE
, FELINAE
, and REPTILIA
. The typeof operator is then used to define three types, Canis, Felis, and Reptil, which correspond to the individual elements of each array. ¡note the use of [number]
!
Type guards are functions that allow to determine whether a variable is of a certain type at runtime. In this example, the isCanis
, isFelis
, isReptil
, and isAllowedAnimal
functions are all type guards that check whether a given animal is of a specific type at runtime.
Play with it on https://www.typescriptlang.org/play