First I have a type definition map all properties into number by using keyof

type Numeric<T> = {
    [K in keyof T]: number
}

Below is a class I will use.

class Entity {
    aNumber: number;
}

Below is a function which accept a generic type argument and a local variable with type Numberic<T>. But when I assigned { aNumber: 1 } it gave a compile error.

const fn = <T extends Entity>() => {
    const n: Numeric<T> = {
//        ^
//        Type '{ aNumber: number; }' is not 
//        assignable to type 'Numeric<T>'
        aNumber: 1
    };
};

I don't know why { aNumber: number; } cannot be assigned to Numeric<T> since the type argument T must be extended from Entity and it must contains a key named aNumber. This means aNumber must be the key of type T and should be able to assigned to Numeric<T>.

2 Answers

3
basarat On

The error message is misleading. However there is an error and that is what TypeScript is catching. Indeed there is nothing wrong with Entity:

type Numeric<T> = {
    [K in keyof T]: number
}
interface Entity {
    aNumber: number
}

// No error
const n: Numeric<Entity> = {
    aNumber: 1
};

However when you say T extends Entity it opens it up for non number values e.g.

type Numeric<T> = {
    [K in keyof T]: number
}
interface Entity {
    aNumber: number
}

// No error
const n: Numeric<Entity> = {
    aNumber: 1
};

interface X extends Entity {
    notANumber: string
}
// Error. Thank you TypeScript
const o: Numeric<X> = {
    aNumber: 1
};

Hence the error when you use a T extends Entity in Numeric.

1
hackape On

The constraint imposed by <T extends Entity> should be considered just the minimum requirement that T should meet. It means "T should at least contain aNumber: number pair".

Let's see const n: T. It means "n should at least contains whatever key-value pairs that are in T".

Now we do know, T has a aNumber: number pair, but remember that's just the minimum requirement. T might as well be { aNumber: number; aString: string }. That's why this will also give you error.

// if you understand this:
const n: { aNumber: number; aString: string } = { aNumber: 42 }  // error, of course
// you see why this is an error:
const n: T = { aNumber: 42 }  // also error

You can never tell what exactly is T. keyof T is fine though, cus we know about at least one of T's key for sure.

const k: keyof T = "aNumber"

To prove my point, let's review a non-generic case. Take @basarat's code for example, here X is not genreic.

type Numeric<T> = {
    [K in keyof T]: number
}

type OptionalNumeric<T> = {
    [K in keyof T]?: number
}

interface Entity {
    aNumber: number
}

interface X extends Entity {
    notANumber: string
}

// Error, because `notANumber` is missing
const o: Numeric<X> = { aNumber: 1 };
// Correct
const o1: Numeric<X> = { aNumber: 1, notANumber: 2 };
// Also correct, because all keys are optional.
const o2: OptionalNumeric<X> = { aNumber: 1 };

Side note. Above should explain your case. However, I do believe there's a bug in TS.

I think if you use OptionalNumeric in your original case, what you want should work. But turns out it doesn't. Should be a defect when generic type parameters involve.