1
// RepositoryBase.ts
export type FieldsMapping<T> = { readonly [k in keyof Required<T>]: string };

export abstract class RepositoryBase<T> {
    protected abstract readonly COLUMNS: FieldsMapping<T>;
}

// UsersRepository.ts
import { FieldsMapping, RepositoryBase } from './RepositoryBase';

interface User {
    firstName: string;
    lastName: string;
    email?: string;
}

export class UsersRepository extends RepositoryBase<User> {
    protected readonly COLUMNS = {
        firstName: 'first_name',
        lastName: 'last_name',
        email: 'email',
        extra: 'non_existing_column',
    };
}

The declaration of COLUMNS in UsersRepository doesn't give any compiling error, even tho extra key doesn't exist on User interface.

If I add the type to COLUMNS in UserRepository, such as: COLUMNS: FieldsMapping<User>, then the error is thrown.

I don't want to redeclare the type on each class which inherits RepositoryBase. Do you have any idea why this is happening? Is maybe due to the abstract? Or maybe is it due to some configuration in tsconfig.json? Any idea how to solve it?

2 Answers 2

2

When you extend an interface or a class with extends, you are allowed to make the subinterface/subclass narrower, both by adding new properties or by narrowing existing properties. So it is perfectly acceptable in TypeScript for the COLUMNS property of UserRepository to be more specific than the COLUMNS property of RepositoryBase<User>. That's what extends is explicitly intended for.

If you choose to annotate the type of the COLUMNS property UserRepository as Record<keyof User, string> (equivalent to your FieldsMapping<User>) and use a new object literal in the assignment, you will get excess property checking and the extra property will be flagged.

If that works for you then you should just do it... it's a bit redundant (you have to write User both in the generic parameter of RepositoryBase<User> and in the annotation for the COLUMNS property), but fairly clean.

Otherwise I don't have any very good fixes for this, mostly because protected properties are hard to manipulate programmatically. (e.g., keyof UserRepository does not include COLUMNS, and thus UserRepository["COLUMNS"] is invalid).

I suppose the closest I could get would be a generic method called something like strictColumns() that would only accept a field mapping with no known extra properties... the method would just return its input, so you'd use it to initialize the COLUMNS property:

abstract class RepositoryBase<T> {
  protected abstract readonly COLUMNS: Record<keyof T, string>;
  strictColumns<
    U extends { [K in keyof T | keyof U]: K extends keyof T ? string : never }
  >(u: U) {
    return u;
  }
}

Like this:

class UsersRepository extends RepositoryBase<User> {
  protected readonly COLUMNS = this.strictColumns({
    firstName: "first_name",
    lastName: "last_name",
    email: "email"
  });
} // okay

class BadUsersRepository extends RepositoryBase<User> {
  protected readonly COLUMNS = this.strictColumns({
    firstName: "first_name",
    lastName: "last_name",
    email: "email",
    extra: "extra" // error! string is not assignable to never
  });
}

This works but the added complexity probably isn't worth the slight reduction in redundancy. Oh well.

Hope that helps. Good luck!

Link to code

Sign up to request clarification or add additional context in comments.

Comments

1
const userLike = {
    firstName: '',
    lastName: '',
    email: '',
    az: ''
};

const user: User = userLike;

Typescript allow this kind of 'flexible' assignment. He's more strict on the declaration.

There is an interesting proposal about this issue: https://github.com/microsoft/TypeScript/issues/12936

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.