Home > database >  Is there a way to use a typescript interface with optional keys, but then concretize that the keys a
Is there a way to use a typescript interface with optional keys, but then concretize that the keys a

Time:10-03

This code:

interface Dog {
  name?: string;
  size?: number;
}

const entity: Dog = {
  name: 'max',
}

const name: string = entity.name;

Causes this error:

Type 'string | undefined' is not assignable to type 'string'.

I can avoid the error by removing the entity type:

interface Dog {
  name?: string;
  size?: number;
}

const entity = {
  name: 'max',
}

const name: string = entity.name;

But then I lose the auto-complete feature.

Is there a way to win both? To have autocomplete, and let the code know which keys are there based on the initializer?

E.g. using Required<Dog> isn't a good solution because I don't want to initialize size. The real use-case I have actually has a much bigger interface.

CodePudding user response:

You can infer it with help of extra function:

interface Dog {
    name?: string;
    size?: number;
}

type IsValidDog<Animal> = Required<Dog> extends Animal ? Animal : never;


const builder = <Animal extends Dog>(animal: IsValidDog<Animal>) => animal

/**
 * Ok
 */
const result = builder({
    name: 'max',
})

result.name // ok
result.size // expected error


const result2 = builder({
    name: 'max',
    size: 42
})
result2.name // ok
result2.size // ok

/**
 * Error
 */

const result3 = builder({ name: 'Sharky', unknown: 2 }) // expected error
const result4 = builder({ nAme: 'Sharky', size: 2 }) // expected error
const result5 = builder({ name: 'Sharky', size: 2, surname: 'Doe' }) // expected error

Playground

builder function expects/allows exact a Dog object. It allows use less properties, because all of them are optional but it disaalow any extra properties to bu used, see result5.

IsValidDog - checks whether Dog with all required props extends passed object interface. If you pass an object with some extra properties, this check will fail.

You can find more validation techniques in my blog

CodePudding user response:

What I do is to define a generic identity check function that can be used for any interfaces:

function identityCheck<T = never>() {
  return <I>(input: I & T) => input as I;
}

then create a concrete check-function for the Dog interface:

const dogIdentity = identityCheck<Dog>();

finally use it to create the constant:

const myDog = dogIdentity({
    name: 'bello',
})
// name is of type string
myDog.name.slice();

Typescript Playground Example

CodePudding user response:

Since the entity is Dog, name of it should be string | undefined. You should:

  • tell the compiler that it's never undefined
  • validate name as usual
interface Dog {
  name?: string;
  size?: number;
}

const entity: Dog = {
  name: 'max',
}

// tell compiler it's never undefined
const name1: string = entity.name!;
if(entity.name) {
  const name = entity.name;
  ...your next codes...
}
// tricky tips
const name3: string = entity.name || "";
const name4: string = entity.name || "NEVER_HAPPEND";
  • Related