ubershmekel
ubershmekel

Reputation: 12818

Is there a way to use a typescript interface with optional keys, but then concretize that the keys are there?

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.

Upvotes: 1

Views: 332

Answers (2)

TmTron
TmTron

Reputation: 19441

Typescript >= 4.9

in typescript 4.9 we can use the satisfies operator:

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

const myDog = {
    name: 'bello',
} satisfies Dog;

const dogName: string = myDog.name;

Typescript < 4.9

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

Upvotes: 2

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

Upvotes: 1

Related Questions