Slevin
Slevin

Reputation: 4222

How to reject arbitrary keys in TypeScript?

I'm using TypeScript for my React + Apollo project and I'm using the graphql-code-generator which generates the following types:

type Maybe<T> = T | null;

type Scalars = {
  ID: string,
  String: string,
  Boolean: boolean,
  Int: number,
  Float: number,
  DateTime: any, 
  ISO8601DateTime: any,
  Date: any,
  Json: any,
};

type RelayNode = {
  id: Scalars['ID'],
};

type Project = RelayNode & {
   __typename?: 'Project',
  number: Scalars['Int'],
  title: Scalars['String']
  projectManagers?: Maybe<Array<{ id: Scalars['String'], name: Scalars['String'] }>>,
};

type GetProjectQuery = (
  { __typename?: 'Query' }
  & { project: Maybe<(
    { __typename?: 'Project' }
    & Pick<Project, 'number' | 'title' | 'projectManagers'>
    )> }
);

type ProjectInput = {
  title?: Maybe<Scalars['String']>,
  number?: Maybe<Scalars['Int']>,
  projectManagerIds?: Maybe<Array<Scalars['String']>>,
};

Now I want to build a form that takes a project for my initial values. An example project may looks like ...

const project: GetProjectQuery['project'] = {
    __typename: 'Project',
    number: 123,
    title: 'My awesome project',
    projectManagers: [{ id: '123', name: 'Me & myself'}]
}

... but my form only allows a ProjectInput and not a Project (since the input variables may differ from the output), so I'm doing this ...

const input: ProjectInput = project

... but this seems to be valid – Typescript doesn't throw any errors. But I want to enforce a warning that the object should not have projectManagers defined. The goal is to enforce an input object hat doesn't have projectManagers but projectManagerIds defined.

I created a minimal test case here, where the "rejection" works:

type Project =  {
  title?: string
  number?: number
};

const project: Project = {
  title: 'foo',
  number: 123,
  foo: 'bar' // this key isn't allowed
}

But I don't get why this doesn't work with my generated typings above.

Here's a complete playground.

Upvotes: 0

Views: 508

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249536

You can't enforce this on a variable. A basic principle of OOP is that a a sub-type is assignable to a base type reference. Since typescript uses structural typing, the base type/sub type relationship is not explicit, but given the structure of GetProjectQuery['project'] and ProjectInput, GetProjectQuery['project'] is a sub-type of ProjectInput

Now typescript sometimes intentionally violates the sub-type is assignable to base-type rule for very specific scenarios:

  1. Excess property checks - These kick in when an object literal is assigned directly to a typed reference and disallows any extra properties.
  2. For weak types (types with not mandatory properties) typescript warns if there is no overlap between types.

Your scenario is neither of these so you will not get an error.

If you want to perform this validation when you calling a function, then we can use a bit of generic type parameter magic to capture the actual type of the parameter and force an error on any extra properties:

function withProjectInput<T extends ProjectInput>(p: T & Record<Exclude<keyof T, keyof ProjectInput>, ["No excess properties allowed here"]>) {

}
withProjectInput(project); // error here

play

Upvotes: 2

Related Questions