3

I'm creating a factory function, which takes a type argument to indicate the type of object to create, plus an parameter argument with parameters describing the object, which is dependent on the type of the object. A simplified example would look like this:

enum EntityType {
  TABLE,
  BUCKET
}
type TableParams = { tableName: string }
type BucketParams = { bucketName: string }

function createEntity (type: EntityType.TABLE, params: TableParams): void
function createEntity (type: EntityType.BUCKET, params: BucketParams): void
function createEntity (type: EntityType, params: TableParams | BucketParams): void {
  switch(type) {
    case EntityType.TABLE:
      console.log('table name:', params.tableName)
      break
    case EntityType.BUCKET:
      console.log('bucket name:', params.bucketName)
      break
  }
}

The function overloads ensure that users can only call the function with the right kind of parameters, depending on the entity type, e.g.

createEntity(EntityType.TABLE, { tableName: 'foo' })
createEntity(EntityType.BUCKET, { bucketName: 'bar' })
createEntity(EntityType.BUCKET, { tableName: 'fox' }) // => No overload matches this call.

The first two calls works just fine, but the 3rd call causes a compilation error: "No overload matches this call".

But how do I ensure type-safety inside the function? I.e. the example above doesn't actually compile because "Property 'tableName' does not exist on type 'TableParams | BucketParams'."

Shouldn't TypeScript be able to deduce the type of the params argument, based on which of the 2 function overloads matches?

1 Answer 1

0

You can get rid of needding the enum and not using a typeguard by putting all params inside of one interface type.

interface EntityData {
    type: { new( ...args : any ) : BaseEntity };
    // Add shared member params
}

interface TableData extends EntityData {
    type: typeof TableEntity;
    tableName: string;
}

interface BucketData extends EntityData {
    type: typeof BucketEntity;
    bucketName: string;
}

class BaseEntity {
    constructor( data : EntityData ) {
        //assign any shared member params here
    }
}

class TableEntity extends BaseEntity {
    public tableName : string;

    constructor( data : TableData ) {
        super(data);
        this.tableName = data.tableName;
        console.log('table name:',this.tableName);
    }
}

class BucketEntity extends BaseEntity {
    public bucketName : string;

    constructor( data : BucketData ) {
        super(data);
        this.bucketName = data.bucketName;
        console.log('bucket name:', params.bucketName);
    }
}

function createEntity( data: TableData ): BaseEntity | undefined;
function createEntity( data: BucketData ): BaseEntity | undefined;
function createEntity( data: EntityData ): BaseEntity | undefined {
    if ( data ) {
        return new data.type(data );
    }
    return undefined;
}

createEntity( { type : TableEntity, tableName : 'foo'});

You can make the above more type safe by using it this way

let myTableData : TableData = { 
    type: TableEntity,
    tableName : 'foo'
}

createEntity( myTableData );

// The above will compile
// The below 2 example will give compile errors

let myTableData2 : TableData = { 
    type: BucketEntity,
    tableName : 'foo'
} // Will give type error for BucketEntity here

let myTableData3 : TableData = {
    type: TableEntity,
    bucketName : 'foo'
}  // Will give error here for bucketName not existing in TableData

If you still want the overloads I would use a typeguard inside of the switch so it is properly typed

enum EntityType {
  TABLE,
  BUCKET
}
type TableParams = { tableName: string }
type BucketParams = { bucketName: string }

function createEntity (type: EntityType.TABLE, params: TableParams): void
function createEntity (type: EntityType.BUCKET, params: BucketParams): void
function createEntity (type: EntityType, params: TableParams | BucketParams): void {
  switch(type) {
    case EntityType.TABLE:
        if( paramsAreTableParams( params ) ) {
            console.log('table name:', params.tableName);
        } else {
            console.log('incorrect params');
        }
        break;
    case EntityType.BUCKET:
        if( paramsAreBucketParams( params ) ) {
            console.log('table name:', params.bucketName);
        } else {
            console.log('incorrect params');
        }
        break;
  }
}

function paramsAreTableParams( params : TableParams | BucketParams ) : params is TableParams {
    return ( params as TableParams).tableName != undefined;
}

function paramsAreBucketParams( params : TableParams | BucketParams ) : params is BucketParams {
    return ( params as BucketParams).bucketName != undefined;
}
Sign up to request clarification or add additional context in comments.

5 Comments

I'm not clear on how I would call the createEntity function in the first code sample. Something like createEntity({ type: TableEntity, tableName: 'foo' }) doesn't work: "Argument of type '{ type: typeof TableEntity; tableName: string; }' is not assignable to parameter of type 'EntityData'."
Sorry fixed I guess you will still need to overrides unless you wanted any type but the first part will allow you to not use if's and switches inside the function
Ok, I get the gist of it and I will play around with your idea some more. As is, your code is not truly type-safe yet, as e.g. createEntity({ type : BucketEntity, tableName : 'foo'}) is accepted by the compiler and produces the output bucket name: undefined.
I updated the above answer a bit
Thank you, @derek-lawrence! Using the entity constructor as the type property on the data interface is a really neat trick. Your updated solution now works beautifully.

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.