5

I want to create an interface whereby I can extend any typescript type to add a property to extend a generic type. For example, create a Confidence<any> type that looks like this:

export interface Confidence<T> extends T{
   confidenceLevel:number
}

It can then be accessed by going:

const date:Confidence<Date> = new Date();
date.confidenceLevel = .9;

This does not seem to be possible (and I get the feeling it is anti-pattern), however I can do

export type Confidence<T> = T & {
   confidenceLevel:number
}

Which appears to accomplish what I want to do though I feel like I'm cheating doing this.

I recognize this "reverse extension" could be problematic if I overwrite a property of the generic type, but is that the only concern? I'm having trouble wrapping my head around this, what is the best way to create a type that merely adds a property?

0

2 Answers 2

12

There is a longstanding feature request, microsoft/TypeScript#2225, which asks for the ability to do interface Foo<T> extends T {...} the way you are asking for it. It's not implemented yet (and might never be). Right now you can only make an interface that extends an object type with statically known keys. The unspecified generic type parameter T does not work because the compiler cannot predict what keys it will have.

Note that you cannot fix this by trying to constrain the type parameter like interface Foo<T extends {}> extends T {....}. That will just give you a misleading error message about constraints, as reported in microsoft/TypeScript#57037, and the same problem where T doesn't have statically known keys persists (because T extends U doesn't restrict the keys of T to the keys of U; it might well have more properties).


You should rest assured that an intersection is a reasonable way of achieving this (with the caveat that you will not be warned if your added property conflicts with a property of T, as you noticed). It is the suggested solution in the aforementioned feature request. Additionally, the TypeScript library typings for Object.assign() use an intersection to represent the type of the result of adding properties to an existing object. Therefore, with Confidence<T> defined like

type Confidence<T> = T & {
  confidenceLevel: number
}

you can easily write a function that produces one of these like this:

function makeConfidence<T>(x: T, confidenceLevel: number): Confidence<T> {
  return Object.assign(x, { confidenceLevel });
}

and use it:

const x = makeConfidence(new Date(), 0.9); // x is a Confidence<Date>;
console.log(x.getFullYear()); // 2020
console.log(x.confidenceLevel); 0.9

Looks good.

Playground link to code

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

3 Comments

Thank you for the sanity check and the note about it being a requested feature! I think I am a little confused about interface vs type, but I think I'll save that for another day.
And if T is an optional type? like type Confidence<T = never> = T & {...}, this will return an undefined and not the rest {...}, any idea how to get around this?
20 min later and found an solution, i will post it in case someone needs it
1

Extending from the Accepted Answer by jcalz:

If you want to make the generic type T optional, you can extend it from Record<string, unknown> and set the value to Record<string, unknown>.

type Confidence<
  T extends Record<string, unknown> = Record<string, unknown>
> = T & {
  confidenceLevel: number
};

And then you will be able to use it without any generic if needed:

interface Bar {
  barLevel: number
};

const foo: Confidence = { confidenceLevel: 100 };
const bar: Confidence<Bar> = { confidenceLevel: 100, barLevel: 2 };

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.