简体   繁体   中英

TypeScript Generic Parameter Type Extending Another Type

I'm having issues compiling the following. it's included in an old project with the comment - "This is a TS1.8 feature. Leave it in to ensure the environment is running the right ts".

  function assign<T extends U, U>(target: T, source: U): T {
        for (let id in source) {
            target[id] = source[id]; // error TS2322: Type 'U[Extract<keyof U, string>]' is not assignable to type 'T[Extract<keyof U, string>]'.
        } 
        return target;
    }

I'm compiling it with the following command

tsc -p tsconfig.json

And this tsconfig.json

{
  "include": [
    "Scripts/TypeScripts/**/*"
  ],
  "compilerOptions": {
    "target": "es5",
    "module": "amd",
    "sourceMap": false,
    "watch": false
  }
}

tsc -v yields Version 3.4.5 .

When I try it in the playground , I also see the error, which makes me think that it is indeed invalid code. However, this poses the questions what was I thinking when I wrote that comment and how come it's been compiling for 2 years (or has it??)

So - my question: Is this valid TS Code? If not, was it ever?

Thanks :-)

This doesn't look valid to me, but I haven't used TS1.8 (I think I started in 2.4 or so). The problem with having T extends U is that, while all of U 's property keys must also exist in T , the values at those property keys could be narrower . That is, given this:

function badAssign<T extends U, U>(target: T, source: U): T {
  for (let id in source) {
    target[id] = source[id] as any; // assert to remove error, but
  }
  return target;
}

You can do this:

interface Ewe {
  w: string;
  x: number;
  y: boolean;
}
interface Tee extends Ewe {
  w: "hey";
  x: 1;
  y: true;
  z: object;
}

const t: Tee = { w: "hey", x: 1, y: true, z: {} };
const u: Ewe = { w: "you", x: 2, y: false };
const newT = badAssign(t, u); // compiles, but
newT.w // "hey" at compile time, "you" at runtime !! 
newT.x // 1 at compile time, 2 at runtime !!
newT.y // true at compile time, false at runtime !!

That's bad... by assigning source[id] to target[id] you are assuming that the property type of target[id] is the same as or wider than the type of source[id] , but when T extends U it means the opposite: target[id] is the same as or narrower than the type of source[id] . So you've lied to the compiler.

The way I'd fix this is by replacing U with Pick<T, K> for some K that extends keyof T . That guarantees that every key of target exists on source as before, and additionally it guarantees that for every key of target , the value of the corresponding property of source is assignable to it:

function assign<T, K extends keyof T>(target: T, source: Pick<T, K>): T {
  for (let id in source) {
    target[id] = source[id]; // okay
  }
  return target;
}

This catches the error of the bad call:

assign(t, u); // now an error, string is not assignable to "hey"

But still lets you use assign() as presumably intended:

let target = { a: "hey", b: 123, c: true };
let source = { a: "you", c: false };
const ret = assign(target, source);

Okay, hope that helps; good luck!

Link to code

I'm guessing you inserted it as a guard to throw errors if TypeScript < v1.8 used.

Via "What's new in TypeScript" for v1.8, subsection " Type parameters as constraints ":

With TypeScript 1.8 it becomes possible for a type parameter constraint to reference type parameters from the same type parameter list. Previously this was an error. This capability is usually referred to as F-Bounded Polymorphism.

Example
function assign<T extends U, U>(target: T, source: U): T {
    for (let id in source) {
        target[id] = source[id];
    }
    return target;
}

let x = { a: 1, b: 2, c: 3, d: 4 };
assign(x, { b: 10, d: 20 });
assign(x, { e: 0 });  // Error

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM