Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make Typescript infer the keys of an object but define type of its value?

Tags:

typescript

I want to define the type of an object but let typescript infer the keys and don't have as much overhead to make and maintain a UnionType of all keys.

Typing an object will allow all strings as keys:

const elementsTyped: { 
    [key: string]: { nodes: number, symmetric?: boolean }
} = {
    square: { nodes: 4, symmetric: true },
    triangle: { nodes: 3 }
}

function isSymmetric(elementType: keyof typeof elementsTyped): boolean {
    return elementsTyped[elementType].symmetric;
}
isSymmetric('asdf'); // works but shouldn't

Inferring the whole object will show an error and allows all kind of values:

const elementsInferred = {
    square: { nodes: 4, symmetric: true },
    triangle: { nodes: 3 },
    line: { nodes: 2, notSymmetric: false /* don't want that to be possible */ }
}

function isSymmetric(elementType: keyof typeof elementsInferred): boolean {
    return elementsInferred[elementType].symmetric; 
    // Property 'symmetric' does not exist on type '{ nodes: number; }'.
}

The closest I got was this, but it don't want to maintain the set of keys like that:

type ElementTypes = 'square' | 'triangle'; // don't want to maintain that :(
const elementsTyped: { 
    [key in ElementTypes]: { nodes: number, symmetric?: boolean }
} = {
    square: { nodes: 4, symmetric: true },
    triangle: { nodes: 3 },
    lines: { nodes: 2, notSymmetric: false } // 'lines' does not exist in type ...
    // if I add lines to the ElementTypes as expected => 'notSymmetric' does not exist in type { nodes: number, symmetric?: boolean }
}

function isSymmetric(elementType: keyof typeof elementsTyped): boolean {
    return elementsTyped[elementType].symmetric;
}
isSymmetric('asdf'); // Error: Argument of type '"asdf"' is not assignable to parameter of type '"square" | "triangle"'.

Is there a better way to define the object without maintaining the set of keys?

like image 990
Jack Avatar asked Feb 08 '19 18:02

Jack


People also ask

How do you define object of objects type in TypeScript?

In TypeScript, object is the type of all non-primitive values (primitive values are undefined , null , booleans, numbers, bigints, strings). With this type, we can't access any properties of a value.

Does TypeScript have type inference?

TypeScript infers types of variables when there is no explicit information available in the form of type annotations. Types are inferred by TypeScript compiler when: Variables are initialized. Default values are set for parameters.

What is contextual typing in TypeScript?

Type inference also works in “the other direction” in some cases in TypeScript. This is known as “contextual typing”. Contextual typing occurs when the type of an expression is implied by its location.


1 Answers

So you want something that infers keys but restricts the value types and uses excess property checking to disallow extra properties. I think the easiest way to get that behavior is to introduce a helper function:

// Let's give a name to this type
interface ElementType {
  nodes: number,
  symmetric?: boolean
}

// helper function which infers keys and restricts values to ElementType
const asElementTypes = <T>(et: { [K in keyof T]: ElementType }) => et;

This helper function infers the type T from the mapped type of et. Now you can use it like this:

const elementsTyped = asElementTypes({
  square: { nodes: 4, symmetric: true },
  triangle: { nodes: 3 },
  line: { nodes: 2, notSymmetric: false /* error where you want it */} 
});

The type of the resulting elementsTyped will (once you fix the error) have inferred keys square, triangle, and line, with values ElementType.

Hope that works for you. Good luck!

like image 188
jcalz Avatar answered Oct 15 '22 20:10

jcalz