Typescript: 建议:类型属性类型

创建于 2014-11-28  ·  76评论  ·  资料来源: microsoft/TypeScript

动机

许多JavaScript库/框架/模式都涉及基于对象的属性名称的计算。 例如Backbone模型,功能转换pluckImmutableJS都基于这种机制。

//backbone
var Contact = Backbone.Model.extend({})
var contact = new Contact();
contact.get('name');
contact.set('age', 21);

// ImmutableJS
var map = Immutable.Map({ name: 'François', age: 20 });
map = map.set('age', 21);
map.get('age'); // 21

//pluck
var arr = [{ name: 'François' }, { name: 'Fabien' }];
_.pluck(arr, 'name') // ['François', 'Fabien'];

在这些示例中,我们可以轻松理解api和基础类型约束之间的关系。
对于骨干模型,对于类型为对象的对象,它只是一种_proxy_:

interface Contact {
  name: string;
  age: number;
}

对于pluck ,这是一个转换

T[] => U[]

其中U是T prop的属性的类型。

但是,我们无法在TypeScript中表达这种关系,并且最终以动态类型结束。

拟议的解决方案

提出的解决方案是为T[prop]类型引入新语法,其中prop是使用返回值或类型参数等类型的函数的自变量。
使用这种新的类型语法,我们可以编写以下定义:

declare module Backbone {

  class Model<T> {
    get(prop: string): T[prop];
    set(prop: string, value: T[prop]): void;
  }
}

declare module ImmutableJS {
  class Map<T> {
    get(prop: string): T[prop];
    set(prop: string, value: T[prop]): Map<T>;
  }
}

declare function pluck<T>(arr: T[], prop: string): Array<T[prop]>  // or T[prop][] 

这样,当我们使用Backbone模型时,TypeScript可以正确地检查getset调用。

interface Contact {
  name: string;
  age: number;
}
var contact: Backbone.Model<Contact>;

var age = contact.get('age');
contact.set('name', 3) /// error

prop常数

约束

显然,常量必须是可以用作索引类型的类型( stringnumberSymbol )。

可分度的情况

让我们看一下我们的Map定义:

declare module ImmutableJS {
  class Map<T> {
    get(prop: string): T[string];
    set(prop: string, value: T[string]): Map<T>;
  }
}

如果T是可索引的,则我们的地图继承此行为:

var map = new ImmutableJS.Map<{ [index: string]: number}>;

现在get的类型get(prop: string): number

审讯

现在,在某些情况下,我很难想到_correct_行为,让我们从Map定义重新开始。
如果不是传递{ [index: string]: number }作为类型参数,我们应该给出
{ [index: number]: number }编译器是否应该引发错误?

如果我们使用pluck和prop的动态表达式而不是常量:

var contactArray: Contact[] = []
function pluckContactArray(prop: string) {
  return _.pluck(myArray, prop);
}

或具有一个常量,该常量不是作为参数传递的类型的属性。
应该调用pluck引发错误,因为编译器无法推断类型T[prop] ,应该将T[prop]解析为{}any ,如果是这样,带有--noImplicitAny的编译器是否会引发错误?

Fixed Suggestion help wanted

最有用的评论

@weswigham @mhegazy ,最近我一直在讨论这个问题; 我们将让您知道我们遇到的任何发展,并且请记住,这只是构思的原型。

目前的想法:

  • keysof Foo运算符从Foo作为字符串文字类型获取属性名称的并集。
  • Foo[K]类型,它指定对于某些类型K是字符串文字类型或字符串文字类型的并集。

从这些基本块中,如果您需要将字符串文字推断为适当的类型,则可以编写

function foo<T, K extends keysof T>(obj: T, key: K): T[K] {
    // ...
}

在这里, K将是keysof T的子类型,这意味着它是字符串文字类型或字符串文字类型的并集。 您为key参数传递的所有内容均应由该文字/文字统一体在上下文中键入,并推断为单例字符串文字类型。

例如

interface HelloWorld { hello: any; world: any; }

function foo<K extends keysof HelloWorld>(key: K): K {
    return key;
}

// 'x' has type '"hello"'
let x = foo("hello");

最大的问题是keysof通常需要“延迟”其操作。 它太急于如何评估类型,这对类型参数是一个问题,例如我发布的第一个示例(即,我们真正要解决的情况实际上是困难的部分:smile :)。

希望能给大家带来更新。

所有76条评论

@NoelAbrahams我真的不认为它是#394的副本,相反,两个功能都非常相似,例如:

 class Model<T> {
    get(prop: memberof T): T[prop];
    set(prop:  memberof T, value: T[prop]): void;
  }

会很理想

@fdecampredon

contact.set(Math.random() >= 0.5 ? 'age' : 'name', 13)

在这种情况下该怎么办?

与本期最后一段的情况差不多。 就像我说过的,我们有多种选择,我们可以报告错误,或为T[prop]推断any T[prop] ,我认为第二种解决方案更合乎逻辑

很棒的建议。 同意,这将是有用的功能。

@fdecampredon ,我相信这是重复的。 请参阅Danmembertypeof

IMO所有这是一个相当狭窄的用例的许多新语法。

@NoelAbrahams不一样。

  • memberof T返回类型,该实例只能是具有有效属性名称为T实例的字符串。
  • T[prop]返回以字符串命名的T属性的类型,该字符串由prop参数/变量表示。

memberof有一个缺点, prop参数的类型应该是memberof T

实际上,我希望基于类型元数据的类型推断功能更加丰富。 但是,这样的运算符以及memberof都是一个好的开始。

这是有趣且可取的。 TypeScript不能很好地处理大量字符串的框架,这显然会有所帮助。

TypeScript在使用大量字符串的框架中效果不佳

真正。 仍然没有改变这是重复建议的事实。

然而,这显然会非常有助于[输入大量字符串的框架]

不确定。 似乎是零碎的,并且对于上面概述的代理对象模式有些特定。 我更希望采用更全面的方法来解决#1003方面的魔术字符串问题。

1003建议使用any作为获取方法的返回类型。 该建议最重要的是,通过添加一种也可以查找值类型的方法-混合将导致如下所示:

declare module ImmutableJS {
  class Map<T> {
    get(prop: memberof T): T[prop];
    set(prop: memberof T, value: T[prop]): Map<T>;
  }
}

@spion ,您是说#394吗? 如果要进一步阅读,您将看到以下内容:

我考虑过返回类型,但将其排除在外,以免使总体建议显得过于挑剔。

这是我最初的想法,但是有问题。 如果有多个类型memberof T变量,则membertypeof T引用哪一个?

get(property: memberof T): membertypeof T;
set(property: memberof T, value: membertypeof T);

这解决了“我指的是哪个参数”问题,但是membertypeof名称似乎是错误的,并且不是操作员对属性名称的狂热。

get(property: memberof T): membertypeof property;
set(property: memberof T, value: membertypeof property);

我认为这样做更好。

get(property: memberof T is A): A;
set(property: memberof T is A, value: A)

不幸的是,尽管我相信最后的建议具有不错的潜力,但不确定我是否有很好的解决方案。

好吧, @ NoelAbrahams在#394中有一条评论,试图或多或少地描述与此内容相同的事物。
现在,我认为比T[prop]可能比此评论的不同主张更为优雅,并且在本期中的主张可能在反思中走得更远。
由于这些原因,我认为不应将其作为重复项关闭。
但是我想我有偏见,因为我是写这个问题的人;)。

@fdecampredon ,更多的是

@NoelAbrahams糟糕,我错过了这一部分。 当然,它们几乎是等效的(这个似乎没有引入另一个通用参数,这可能会或可能不会成为问题)

看过Flow之后,我认为它会更强大一些,它具有更强大的字体系统和特殊类型,而不是临时字体缩小。

例如,我们用get(prop: string): Contact[prop]表示的只是一系列可能的重载:

interface Map {
  get(prop : string) : Contact[prop];
}

// is morally equivalent to 

interface Map {
  get(prop : "name") : string;
  get(prop : "age") : number;
}

假设存在&类型运算符(交集类型),则该类型等效于

interface Map {
   get : (prop : "name") => string & (prop : "age") => number;
}

现在,我们已经将非泛型情况转换为仅类型表达式,并且没有特殊处理(没有[prop] ),我们可以解决参数问题。

想法是从类型参数某种程度上生成此类型。 我们可能会定义一些特殊的伪泛型类型$MapProperties$Name$Value以仅类型表达式(无特殊语法)表示生成的类型,同时仍提示类型检查器应该做些什么。

class Map<T> {
   get : $MapProperties<T, (prop : $Name) => $Value>
   set : $MapProperties<T, (prop : $Name, val : $Value) => void>
}

它可能看起来很复杂,几乎是模板化的,或者是穷人依赖的类型,但是当有人希望类型依赖于值时,就无法避免。

另一个有用的领域是遍历类型对象的属性:

interface Env {
 // pretend this is an actually interesting type
};

var actions = {
  action1: function (env: Env, x: number) : void {},
  action2: function (env: Env, y: string) : void {}
};

// actions has type { action1: (Env, number) => void; action2: (Env, string) => void; }
var env : Env = {};
var boundActions = {};
for (var action in actions) {
  boundActions[action] = actions[action].bind(null, env);
}

// boundActions should have type { action1: (number) => void; action2: (string) => void; }

从理论上讲,这些类型应该至少可以推断(有足够的类型信息来推断for循环的结果),但可能也很麻烦。

请注意,下一版本的react将大大受益于该方法,请参阅https://github.com/facebook/react/issues/3398

https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -64944856一样,当字符串由除字符串文字以外的表达式提供时,此功能会由于停止问题而迅速失效。 但是,是否仍可以使用从ES6 Symbol问题中学习(#2012)来实现其基本版本?

已批准。

我们将要在实验分支中进行尝试,以了解语法。

只是想知道将执行哪个版本的提案? 即T[prop]将在呼叫站点进行评估并急切地替换为具体类型,还是会成为类型变量的新形式?

我认为我们应该依靠#3779中定义的更通用,更简洁的语法。

interface Map<T> {
  get<A>(prop: $Member<T,A>): A;
  set<A>(prop: $Member<T,A>, value: A): Map<T>;
}

还是不可以推断A的类型?

只是想说一下,我在等待正常解决方案时使用了一个小的代码生成工具,以简化TS与ImmutableJS的集成: https :

我还要指出,具有成员类型的解决方案可能不适用于ImmutableJS:

interface Profile {
  firstName: string 
}

interface User {
  profile: Profile  
}

let a: Map<User> = fromJS(/* ... */);
a.get('profile') // Type will be Profile, but the real type is Map<Profile>!

@ s-panferov这样的事情可能起作用:

interface ImmutableMap<T> {
    get<A extends boolean | number | string>(key : string) : A;
    get<A extends {}>(key : string) : ImmutableMap<A>;
    get<E, A extends Array<any>>(key : string) : ImmutableList<E>;
}

interface Profile {

}

interface User {
    name : string;
    profile : Profile;
}

var map : ImmutableMap<User>;

var name = map.get<string>('name'); // string
var profile = map.get<Profile>('profile'); // ImmutableMap<Profile>

这不会排除DOM节点或Date对象,但完全不应在不可变结构中使用它们。 https://github.com/facebook/immutable-js/wiki/Converting-from-JS-objects

从最佳解决方法逐步进行工作

我认为首先从当前可用的最佳解决方法开始,然后从那里逐步解决是很有用的。

假设我们需要一个函数,该函数返回任何包含非原始对象的属性的值:

function getProperty<T extends object>(container: T; propertyName: string) {
    return container[propertyName];
}

现在我们希望返回值具有目标属性的类型,因此可以为其类型添加另一个通用类型参数:

function getProperty<T extends object, P>(container: T; propertyName: string) {
    return <P> container[propertyName];
}

因此,带有类的用例看起来像:

class C {
    member: number;
    static member: string;
}

let instance = new C();
let result = getProperty<C, typeof instance.member>(instance, "member");

并且result将正确接收类型number

但是,调用中似乎有对“成员”的重复引用:一个在type参数中,另一个是_literal_字符串,它将始终接收目标属性名称的字符串表示形式。 该建议的想法是可以将这两个统一为一个仅接收字符串表示形式的单一类型参数。

因此,在这里需要注意的是,该字符串还可以充当_generic parameter_,并且需要以文字形式传递才能起作用(其他情况可能会静默忽略其值,除非可以使用某种形式的运行时反射)。 因此,为了使语义清晰,需要某种方式来表示泛型参数和字符串之间的关系:

function getProperty<T extends object, PName: string = propertyName>(container: T; propertyName: string) {
    return <T[PName]> container[propertyName];
}

现在,调用将如下所示:

let instance = new C();
let result = getProperty<C>(instance, "member");

在内部将解析为:

let result = getProperty<C, "member">(instance, "member");

但是,由于C包含实例和名为member静态属性,因此种类较高的通用表达式T[PName]是不明确的。 由于此处的意图主要是将其应用于实例的属性,因此可以在内部使用诸如建议的typeon运算符之类的解决方案,这也可以改善语义,因为有人将T[PName]解释为表示_value reference_,不一定是类型:

function getProperty<T extends object, PName: string = propertyName>(container: T; propertyName: string) {
    return <typeon T[PName]> container[propertyName];
}

(如果T是兼容的接口类型,这也将起作用,因为typeon支持接口)

要获得静态属性,将使用构造函数本身调用该函数,并且构造函数类型应作为typeof C传递:

let result = getProperty<typeof C>(C, "member"); // Note it is called with the constructor object

(内部typeon (typeof C)解析为typeof C因此可以使用)
result现在将正确接收类型string

为了支持其他类型的属性标识符,可以将其重写为:

type PropertyIdentifier = string|number|Symbol;

function getProperty<T extends object, PName: PropertyIdentifier = propertyName>(container: T; propertyName: PropertyIdentifier) {
    return <typeon T[PName]> container[propertyName];
}

因此,需要几种不同的功能来支持此功能:

  • 允许将文字值作为通用参数传递。
  • 允许这些文字接收函数参数的值。
  • 允许使用形式更高级的泛型。
  • 允许通过较高种类的泛型通过_generic_类型上的文字泛型参数来引用属性类型。

重新思考问题

现在,让我们回到原始的解决方法:

function getProperty<T extends object, P>(container: T; propertyName: string) {
    return <P> container[propertyName];
}

此解决方法的一个优点是,可以轻松扩展它以支持更复杂的路径名,不仅引用直接属性,还引用嵌套对象的属性:

function getPath<T extends object, P>(container: T; path: string) {
    ... more complex code here ...

    return <P> resultValue;
}

现在:

class C {
    a: {
        b: {
            c: string[];
        }
    }
}
let instance = new C();
let result = getPath<C, typeof instance.a.b.c>(instance, "a.b.c");

result会在此处正确获得类型string[]

问题是,是否也应支持嵌套路径? 如果键入时没有自动完成功能,该功能真的有用吗?

这表明可能会有不同的解决方案,它将在另一个方向上起作用。 回到原始的解决方法:

function getProperty<T extends object, P>(container: T; propertyName: string) {
    return <P> container[propertyName];
}

从另一个方向来看,可以使用类型引用本身并将其转换为字符串:

function getProperty<T extends object, P>(container: T; propertyName: string = @typeReferencePathOf(P)) {
    return <P> container[propertyName];
}

@typeReferencePathOf在概念上与nameOf类似,但适用于通用参数。 它将使用接收到的类型引用typeof instance.member (或typeon C.member ),提取属性路径member ,并在编译时将其转换为文字字符串"member"时间。 较不专业的互补@typeReferenceOf将解析为完整字符串"typeof instance.member"

所以现在:

getProperty<C, typeof instance.subObject>(instance);

将解决:

getProperty<C, typeof instance.subObject>(instance, "subObject");

getPath的类似实现:

getPath<C, typeof instance.a.b.c>(instance);

将解决:

getPath<C, typeof instance.a.b.c>(instance, "a.b.c");

由于@typeReferencePathOf(P)仅设置为默认值。 如果需要,可以手动指定参数:

getPath<C, SomeTypeWhichIsNotAPath>(instance, "member.someSubMember.AnotherSubmember.data");

如果不提供第二个类型参数,则@typeReferencePathOf()可以解析为undefined也可以解析为空字符串"" 。 如果提供了一种类型,但没有内部路径,则它将解析为""

这种方法的优点:

  • 不需要更高种类的泛型。
  • 不需要允许将文字用作通用参数。
  • 不需要以任何方式扩展泛型。
  • 键入typeof表达式时允许自动完成,并使用现有的类型检查机制来验证它,而不需要在编译时从字符串表示形式转换它。
  • 也可以在泛型类中使用。
  • 不需要typeon (但可以支持)。
  • 可以在其他情况下应用。

+1

真好! 我已经开始写一个解决这个问题的大建议,但是找到了这个讨论,所以让我们在这里讨论。

这是我未提交的问题的摘录:

问题:应该如何在.d.ts中声明以下函数,以便在应该使用的上下文中使用?

function mapValues(obj, fn) {
      return Object.keys(obj)
          .map(key => ({key, value: fn(obj[key], key)}))
          .reduce((res, {key, value}) => (res[key] = value, res), {})
}

几乎每个通用的“ utils”库中都可以找到此功能(稍有变化)。

mapValues有两种不同的用例:

一种。 _Object-as- a_ _Dictionary _,其中属性名称是动态的,值具有相同的一种类型,并且该函数通常是单态的(意识到这种类型)

var obj = {'a': 1, 'b': 2, 'c': 3};
var res = mapValues(x, val => val * 5); // {a: 5, b: 10, c: 15}
console.log(res['a']) // 5

b。 _Object-as- a_ _Record _,其中每个属性都有其自己的类型,而该函数是参数多态的(通过实现)

var obj = {a: 123, b: "Hello", c: true};
var res = mapValues(p, val => [val]); // {a: [123], b: ["Hello"], c: [true]}
console.log(res.a[0].toFixed(2)) // "123.00"

当前TypeScript对mapValues的最佳可用检测是:

declare function mapValues<T1, T2>(
    obj: {[key: string]: T1},
    fn: (arg: T1, key: string) => T2
): {[key: string]: T2};

不难看出该签名与_Object-as- a__ _Dictionary _用例完全匹配,而在_Object-as-a-_的情况下应用时会产生令人惊讶的类型推断结果1 _记录_是意图。

{p1: T1, p2: T2, ...pn: Tn}将被强制为{[key: string]: T1 | T2 | ... | Tn }合并所有类型并有效地丢弃有关记录结构的所有信息:

  • 正确且预期为2的正确console.log(res.a[0].toFixed(2))将被编译器拒绝;
  • 接受的console.log((<number>res['a'][0]).toFixed(2))具有两个不受控制且易于出错的位置:类型转换和任意属性名称。

1,2 -为程序员谁从ES到TS迁移


解决此问题的可能步骤(以及解决其他问题)

1.在字符串文字类型的帮助下引入可变参数记录

type Numbers<p extends string> =  { ...p: number };
type NumbersOpt<p extends string> = {...p?: number };
type ABC = "a" | "b" | "c";
type abc = Numbers<ABC> // abc =  {a: number, b: number, c: number} 
type abcOpt = NumbersOpt<ABC> // abcOpt =  {a?: number, b?:number, c?: number}

function toFixedAll<p extends string>(obj: {...p: number}, precision):{...p: string}  {
      var result: {...p: string} = {} as any;
      Object.keys(obj).forEach((p:p) => {
           result[p] = obj[p].toFixed(precision);
      });
      return result;
}

var test = toFixedAll({x:5, y:7}, 3); // { x: "5.00", y: "6.00" },  p inferred as "x"|"y"
console.log(test.y.length) // 4   test.y: string
2.允许形式类型与其他受限制扩展“字符串”的形式类型下标

例:


declare function mapValues<p extends string, T1[p], T2[p]>(
     obj:{...p: T1[p]}, fn:(arg: T1[p]) => T2[p]
): {...p: T2[p]};
3.允许销毁正式类型

例:

class C<Array<T>> {
     x: T;
}   

var v1: C<string[]>;   // v1.x: string

通过所有三个步骤,我们可以编写

declare module Backbone {

  class Model<{...p: T[p]}> {
    get(prop: p): T[p];
    set(prop: p, value: T[p]): void;
  }
}

declare module ImmutableJS {
  class Map<{...p: T[p]}> {
    get(prop: p): T[p];
    set(prop: p, value: T[p]): this;
  }
}

declare function pluck<p extends string, T[p]>(
    arr: Array<{...p:T[p]}>, prop: p
): Array<T[p]> 

我知道语法比@fdecampredon提出的语法更冗长
但我无法想象如何用原始提案来表示mapValuescombineReducers的类型。

function combineReducers(reducers) {
  return (state, action) => mapValues(reducers, (reducer, key) => reducer(state[key], action))
}

根据我的建议,其声明将如下所示:

declare function combineReducers<p extends string, S[p]>(
    reducers: { ...p: (state: S[p], action: Action) => S[p] }
): (state: { ...p: S[p] }, action: Action) => { ...p: S[p] };

原始提案可以表达吗?

@ spion@ fdecampredon

@Artazor绝对是一个进步。 现在似乎足以表达我对此功能的基准用例:

假设user.iduser.name是相应地包含numberstring数据库列类型,即Column<number>Column<string>

编写函数select的类型,该函数的类型为:

select({id: user.id, name: user.name})

并返回Query<{id: number; name: string}>

select<T>({...p: Column<T[p]>}):Query<T>

不确定如何检查和推断。 似乎存在问题,因为类型T不存在。 返回类型是否需要以非结构化形式表示? 即

select<T>({...p: Column<T[p]>}):Query<{...p:T[p]}>

@Artazor忘了问, p extends string参数真的必要吗?

这样的东西不够吗?

select<T[p]>({...p: Column<T[p]>}):Query<{...p:T[p]}>

即对于{...p:Column<T[p]>}所有类型变量T [p]

编辑:另一个问题,如何将其检查正确性?

@spion我有您的观点-您误解了我(但这是我的错),因为T[p]我的意思是完全不同的东西,您会看到p extends string在这里至关重要。 让我深入解释我的想法。

让我们考虑数据结构的四个用例: ListTupleDictionaryRecord 。 我们将它们称为_conceptual数据结构_,并使用以下属性对其进行描述:

概念数据结构通过访问
一种。 位置
(数)
b。 键
(串)

项目
1.变量
(每个项目的角色相同)
清单字典
2.固定
(每个项目都有其独特的作用)
元组记录

在JavaScript中,这四种情况由两种存储形式支持: a Arraya2Object —和b1b2Object

_注1 _。 老实说,说TupleArray支持是不正确的,因为唯一的“ true”元组类型是arguments对象的类型。不是Array 。 然而,它们或多或少是可以互换的,特别是在解构和Function.prototype.apply ,它们为元编程打开了大门。 TypeScript还利用数组对元组进行建模。

_注2 _。 尽管数十年后以ES6 Map形式引入了具有任意键的字典的完整概念,但Brendan Eich最初决定将Record和有限的Dictionary ( (仅字符串键)在Object (其中obj.prop相当于obj["prop"] )的同一个条件下成为了整个语言中最具争议的元素(在我看来)。
这既是JavaScript语义的诅咒,也是祝福。 它使反射变得微不足道,并鼓励程序员以几乎为零的精神成本(即使不注意!)在_programming_和_meta-programming_级别之间自由切换。 我相信,作为脚本语言,这是JavaScript成功的关键部分。

现在是时候让TypeScript提供表达这种奇怪语义类型的方式了。 当我们认为在_programming_级别时,一切正常:

编程级别的类型通过访问
一种。 位置
(数)
b。 键
(串)

项目
1.变量
(每个项目的角色相同)
T []{[key:string]:T}
2.固定
(每个项目都有其独特的作用)
[T1,T2,...]{key1:T1,key2:T2,...}

但是,当我们切换到_meta-programming_级别时,固定在_programming_级别的内容突然变得可变了! 在这里,我们突然意识到元组和函数签名之间的关系,并提出了诸如#5453(可变参数种类)之类的东西,实际上它仅覆盖了元编程需求的一小部分(但非常重要)—通过赋予功能以破坏形式化形式来组合签名参数分为rest-args类型:

     function f<T>(a: number, ...args:T) { ... }

    f<[string,boolean]>(1, "A", true);

如果#6018也将被实现,则Function类可能看起来像

declare class Function<This, TArgs, TRes> {
        This::(...args: TArgs): TRes;
        call(self: This, ...args: TArgs): TRes;
        apply(self: This, args: TArgs): TRes;
        // bind needs also formal pattern matching:
        bind<[...TPartial, ...TCurried] = TArgs>(
           self: This, ...args: TPartial): Function<{}, TCurried, TRes> 
}

很棒,但是无论如何都不完整。

例如,假设我们要为以下函数分配正确的类型:

function extractAndWrapAll(...args) {
     return args.map(x => [x]);
}

// wrapAll(1,"A",true) === [[1],["A"],[true]]

使用建议的可变参数种类,我们无法转换签名的类型。 在这里,我们需要更强大的功能。 实际上,希望具有可以对类型进行操作的编译时函数(如Facebook的Flow)。 不过,我敢肯定,只有在(如果有)TypeScript的类型系统足够稳定以将其公开给最终程序员,而不会在下一次较小的更新中破坏用户土地的重大风险时,这才有可能。 因此,我们需要的东西远没有完整的元编程支持那么激进,但仍然能够处理已证明的问题。

为了解决这个问题,我想介绍_object signature_的概念。 大概是

  1. 对象的属性名称的集合,或者
  2. 字典键的集合,或者
  3. 数组的数字索引集
  4. 元组的数字位置集合。

理想情况下,最好具有整数文字类型以及字符串文字类型来表示数组或元组的签名,例如

type ZeroToFive = 0 | 1 | 2 | 3 | 4 | 5;
// or
type ZeroToFive = 0 .. 5;   // ZeroToFive extends number (!)

整数文字的规则与字符串文字的规则一致;

然后,我们可以引入签名抽象语法,该语法与对象rest / spread语法完全匹配:

{...Props: T }

有一个重要的例外:这里Props是通用量词下绑定的标识符:

<Props extends string> {...Props: T }  // every property has type T
<Index extends number> {...Index: T }  // every item has type T  
// the same as T[]

因此,它是在类型词法范围内引入的,并且可以在该范围内的任何地方用作类型名称。 但是,它的用法是双重的:当使用它作为独立类型时,它代表字符串(或数字)的子类型。

declare class Object {
      static keys<p extends string, q extends p>(object{...q: {}}): p[];
}

这是最复杂的部分:_Key Dependent Types_

我们介绍一种特殊的类型构造: T for Prop (我想使用这种语法,而不是让您感到困惑的T [Prop]),其中Prop是保存对象签名抽象的类型变量的名称。 例如<Prop extends string, T for Prop>介绍两个形式类型分为类型词法作用域的PropT其中已知,对于每个特定值pProp将会是它自己的类型T

我们并不是说某个地方的对象具有属性Props ,它们的类型是T ! 我们仅介绍两种类型之间的功能依赖关系。 类型T与类型Props的成员相关,仅此而已!

它使我们有可能编写诸如

function unwrap<P extends string, T for P>(obj:{...P: Maybe<T>}): Maybe<{...P: T}> {
  ...
}

unwrap({a:{value:1}, b:{value:"A"}, c:{value: true}}) === { a: 1, b: "A", c: true }
// here actual parameters will be inferred as 
unwrap<"a"|"b"|"c", {a: number, b: string, c: boolean}>

但是,第二个参数不是对象,而是标识符到类型的抽象映射。 从这个角度来看,当P是数字的子类型时, T for P可以用来表示元组的抽象类型序列( @JsonFreeman ?)

T是内部使用的地方{...P: .... T .... }它代表只有一个特定类型的地图。

这是我的主要想法。
急切地等待任何问题,想法,批评-)

正确,因此extends string在一种情况下要考虑数组(和可变参数),在另一种情况下要考虑字符串常量(作为类型)。 甜!

我们并不是说某个地方的对象具有属性Props,其类型为T! 我们仅介绍两种类型之间的功能依赖关系。 类型T与类型Props的成员相关,仅此而已!

我的意思不是,我的意思更多,因为它们的类型是T [p],即由p索引的类型的字典。 如果那是一个很好的直觉,我会保留的。

虽然总的来说,语法可能需要做更多的工作,但是总体思路看起来很棒。

是否可以为可变参数写unwrap

编辑:没关系,我只是意识到您对可变参量种类的拟议扩展解决了这一问题。

大家好,
我很困惑如何解决我的问题之一,并找到了这个讨论。
问题是:
我有RpcManager.call(command:Command):Promise<T>方法,用法是这样的:

RpcManager.call(new GetBalance(123)).then((result) => {
 // here I want that result would have a type.
});

我认为解决方案可能是这样的:

interface Command<T> {
    responseType:T;
}

class GetBalance implements Command<number> {
    responseType: number; // somehow this should be avoided. maybe Command should be abstract class.
    constructor(userId:number) {}
}

class RpcManager {
    static call(command:Command):Promise<typeof command.responseType> {
    }
}

or:

class RpcManager {
    static call<T>(command:Command<T>):Promise<T> {
    }
}

有什么想法吗?

@ antanas-arvasevicius该示例中的最后一个代码块可以完成您想要的操作。

听起来您似乎对如何完成特定任务有更多疑问; 如果您发现编译器错误,请使用Stack Overflow或提出问题。

嗨,瑞安,谢谢您的回复。
我已经尝试了最后的代码块,但是没有用。

快速演示:

interface Command<T> { }
class MyCommand implements Command<{status:string}> { }
class RPC { static call<T>(command:Command<T>):T { return; } }

let response = RPC.call(new MyCommand());
console.log(response.status);

//output: error TS2339: Property 'status' does not exist on type '{}'.
//tested with: Version 1.9.0-dev.20160222

抱歉,我没有使用Stack Overflow,但我认为这与该问题有关:)
我应该就这个话题开一个新问题吗?

未使用的泛型类型参数会阻止推理。 通常,您应该_never_在类型声明中使用未使用的类型参数,因为它们毫无意义。 如果消耗T ,那么一切都将正常工作:

interface Command<T> { foo: T }
class MyCommand implements Command<{status:string}> { foo: { status: string; } }
class RPC { static call<T>(command:Command<T>):T { return; } }

let response = RPC.call(new MyCommand());
console.log(response.status);

太神奇了! 谢谢!
我没想到类型参数可以放在泛型类型和
TS将提取它。
2016年2月22日晚上11:56,“ Ryan Cavanaugh” [email protected]写道:

未使用的泛型类型参数会阻止推理。 在
通常,您应该_never_在类型中具有未使用的类型参数
声明,因为它们毫无意义。 如果你消耗T,一切都只是
作品:

接口命令{foo:T}类MyCommand实现Command <{status:string}> {foo:{status:string; }}类RPC {静态调用(命令:命令):T {return; }}
let response = RPC.call(new MyCommand());
console.log(response.status);

-
直接回复此电子邮件或在GitHub上查看
https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -187404245

@ antanas-arvasevicius,如果要创建RPC样式的API,我有一些文档可能对您有用: https :

上面的方法似乎是:

  • 相当复杂
  • 不要处理代码中字符串的使用。 string不能“查找所有引用”,也不能重构(例如重命名)。
  • 有些仅支持“第一级”属性,而不支持更复杂的表达式(某些框架支持)。

这是另一个受C#表达式树启发的想法。 这只是一个粗略的想法,没有深思熟虑! 语法很糟糕。 我只想看看这是否激发了某人。

假设我们有一种特殊的字符串来表示表达式。
我们称之为type Expr<T, U> = string
其中T是起始对象类型,而U是结果类型。

假设我们可以使用一个lambda创建Expr<T,U>的实例,该实例接受一个类型T并对其执行成员访问。
例如: person => person.address.city
发生这种情况时,整个lambda都会编译为一个字符串,其中包含对参数的任何访问,在本例中"address.city"

您可以改用纯字符串,将其视为Expr<any, any>

在语言中使用特殊的Expr类型可以实现以下功能:

function pluck<T, U>(array: T[], prop: Expr<T, U>): U[];

let numbers = pluck([{x: 1}, {x: 2}], p => p.x);  // number[]
// compiles to:
// let numbers = pluck([..], "x");

这基本上是C#中使用的表达式的一种有限形式。
您认为这可以完善并引导到有趣的地方吗?

@fdecampredon @RyanCavanaugh

_( @ jods4-很抱歉,我在这里没有回复您的建议。希望它不会被评论“

我认为此功能(“类型属性类型”)的命名非常容易混淆,很难理解。 甚至很难弄清这里描述的概念是什么以及它的含义!

首先,并非所有类型都具有属性! undefinednull不会(尽管这只是类型系统的最新添加)。 像numberstringboolean很少被属性索引(例如2 [“ prop”]?尽管这似乎可行,但几乎总是错误的)

我建议通过字符串文字值来命名此问题引用现有的类型。

如果在特定用例的上下文之外尽可能简单地描述和举例说明这将是非常有益的:

interface MyInterface {
  prop1: number;
  prop2: string;
}

let prop1Name = "prop1";
type Prop1Type = MyInterface[prop1Name]; // Prop1Type is now 'number'

let prop2Name = "prop2";
type Prop2Type = MyInterface[prop2Name]; // Prop2Type is now 'string'

let prop3Name = "prop3";
type NonExistingPropType = MyInterface[prop3Name]; // Compilation error: property 'prop3' does not exist on 'MyInterface'.

let randomString = createRandomString();
type NotAvailablePropType = MyInterface[randomString]; // Compilation error: value of 'randomString' is not known at compile time.

_Edit:为了正确实现此目的,似乎编译器必须确定分配给变量的字符串在它初始化的点和在类型表达式中引用的点之间没有变化。 我不觉得这很容易吗? 关于运行时行为的这种假设是否始终成立?_

_Edit 2:也许在与变量一起使用时,这仅适用于const

我不确定最初的意图是仅允许在字符串文字的特殊情况下将属性引用传递给函数或方法参数吗? 例如

function func(someString: string): MyInterface[someString] {
  ..
}

let x = func("prop"); // x gets the type of MyInterface.prop

我是否已经超出了最初的意图?

如果函数参数不是字符串文字(即在编译时未知),将如何处理? 例如

let x = func(getRandomString()); // What type if inferred for 'x' here?

它会出错,还是默认为any

(PS:如果确实是这个意图,那么我建议通过字符串文字函数或方法参数将此问题重命名为

这是一个简单的基准示例(足以说明),显示了需要启用此功能的功能:

编写此函数的类型,其中:

  • 接受一个包含诺言字段的对象,并且
  • 返回相同的对象,但所有字段都已解析,每个字段都具有正确的类型。
function awaitObject(obj) {
  var result = {};
  var wait = Object.keys(obj)
    .map(key => obj[key].then(val => result[key] = val));
  return Promise.all(wait).then(_ => result)
}

在以下对象上调用时:

var res = awaitObject({a: Promise.resolve(5), b: Promise.resolve("5")})

结果res的类型应为{a: number; b: string}


使用此功能(由@Artazor充实),签名将为

awaitObject<p, T[p]>(obj: {...p: Promise<T[p]>}):Promise<{...p: T[p]}>

编辑:修复了上面的返回类型,丢失了Promise<...> ^^

@spion

感谢您尝试提供一个示例,但是在这里我找不到一个合理而简洁的规范以及一组明显的简化示例,这些示例与实际应用程序是“分离的”,并试图突出其语义以及各种工作方式,甚至是基本的东西:

function func<T extends object>(name: string): T[name] {
 ...
}
  1. 很少有人努力提供有效的说明性标题(因为我已经提到过'Type Property Type'令人困惑,并且此功能的名称也不是很解释性,它实际上与任何特定类型无关,而与类型引用有关)。 我最好的建议标题是_Interface属性类型通过字符串文字函数或方法arguments_的引用(假设我没有误解其范围)。
  2. 没有明确提及如果name在编译时未知,为空或未定义(例如func<MyType>(getRandomString())func<MyType>(undefined)
  3. 没有明确提到如果T是像numbernull这样的原始类型会发生什么。
  4. 没有提供有关这是否仅适用于函数以及T[name]可以在函数主体中使用的详细信息。 在这种情况下,如果在函数体中重新分配了name ,那么在编译时可能不再知道其值会发生什么呢?
  5. 没有语法的真正说明:例如T["propName"]T[propName]本身是否也可以工作? 我没有引用参数或变量,这是很有用的!
  6. 没有提及或讨论T[name]可以在其他参数中使用,甚至可以在功能范围之外使用,例如
function func<T extends object>(name: string, val: T[name]) {
 ...
}
type A = { abcd: number };
const name = "abcd";
let x: A[name]; // Type of 'x' resolves to 'number'

_7。 没有真正讨论使用附加的泛型参数和typeof的相对简单的解决方法(尽管已提及,但是在该功能已被接受之后相对较晚):

function get<T, V>(obj: T, propName: string): V {
    return obj[propName];
}

type MyType = { abcd: number };
let x: MyType  = { abcd: 12 };

let result = get<MyType, typeof x.abcd>(x, "abcd"); // Type of 'result' is 'number'

结论:这里没有真正的建议,只有一组用例演示。 我感到惊讶的是,TypeScript团队已经完全接受或什至获得了积极的兴趣,因为我不相信这会符合他们的标准。 很抱歉,我在这里听起来有点刺耳(不是我的典型),但是我的批评都没有针对个人或针对任何特定个人的。

我们真的想在元编程方面走得那么远吗?

JS是一种动态语言,代码可以以任意方式操纵对象。
我们在类型系统中可以建模的内容有局限性,很容易想到您无法在TS中建模的功能。
问题是:走多远合理且有用?

来自@spion的最后一个示例( awaitObject )是在同一时间:

  • 从语言的角度来看, _非常复杂(我在这里同意
  • 在适用性方面非常狭窄。 该示例选择了特定的约束,并且不能很好地概括。

顺便说一句, @ spion您没有正确获得示例的返回类型...它返回了Promise

我们与最初的问题相去甚远,最初的问题是关于输入API并采用表示字段的字符串,例如_.pluck
应该支持那些API,因为它们很普通,而且没有那么复杂。 我们不需要像{ ...p: T[p] }这样的元模型。
OP中有几个示例,我在nameof issue中的评论中来自Aurelia的更多示例。
一种不同的方法可以覆盖这些用例,有关可能的想法,请参见上面

@ jods4一点也不窄。 它可以用于目前无法在类型系统中建模的许多事物。 我想说它是TS类型系统中最后遗失的大部分之一。 @Artazor https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -177287714上面有很多真实的示例,上面是更简单的东西,sql查询生成器“ select”示例,依此类推。

这是bluebird中的awaitObject 这是一种真正的方法。 该类型当前无用。

一个更通用的解决方案会更好。 它适用于简单和复杂的情况。 动力不足的解决方案将在一半的情况下被发现,如果将来有必要采用更通用的解决方案,则很容易引起兼容性问题。

是的,它需要做更多的工作,但是我也认为@Artazor在分析和探索我们以前从未想到的所有方面方面做得非常出色。 我不会留下我们偏离原始问题的想法,我们只会对此有更好的了解。

我们会尝试使用一个更好的名称,但我通常不会正确地执行这些操作。 “对象泛型”怎么样?

@spion ,仅用于

awaitObject<p,T[p]>(obj: {...p:Promise<T[p]>}): Promise<{...p:T[p]}>

(您错过了输出签名中的Promise)
-)

@ jods4 ,我同意@spion
有很多实际问题,其中{ ...p: T[p] }是解决方案。 举一个例子:react + flux / redux

@spion @Artazor
我只是担心,要精确指定它变得非常复杂。

这个问题背后的动机已经漂移。 最初,它是关于支持接受字符串以表示对象字段的API。 现在,主要讨论以动态方式将对象用作地图或记录的API。 请注意,如果我们能用一块石头杀死两只鸟,那就全力以赴了。

如果回到带字符串问题的API,我认为T[p]并不是一个完整的解决方案(我之前说过为什么)。

_只是为了保证正确性_ awaitObject也应该接受非Promise属性,至少Bluebird props可以。 现在我们有了:

awaitObject<p,T[p]>(obj: { ...p: T[p] | Promise<T[p]> }): Promise<T>

我将返回类型更改为Promise<T>因为我希望这种表示法有效。
还有其他重载,例如,对此类对象采取Promise的重载(其签名更加有趣)。 因此,这意味着必须将{ ...p }表示法比任何其他类型的运算法则都更糟糕。

指定所有这些将是艰巨的工作。 我想说,这是下一步,如果您想推动这一步。

@spion @ jods4

我想提一下,此功能与泛型无关,与更高类型的多态类型无关。 这只是通过包含类型(带有一些“高级”扩展名,将在下面描述)来引用属性类型的语法,在概念上与typeof相差不远。 例:

type MyType = { abcd: number };
let y: MyType["abcd"]; // Technically this could also be written as MyType.abcd

现在比较:

type MyType = { abcd: number };
let x: MyType;

let y: typeof x.abcd;

typeof有两个主要区别。 与typeof ,它适用于类型(至少是非原始类型,这里经常省略这一事实),而不是实例。 此外(这是一件很重要的事情),它被扩展为支持使用文字字符串常量(必须在编译时知道)作为属性路径描述符和泛型:

const propName = "abcd";
let y: MyType[propName];
// Or with a generic parameter:
let y: T[propName];

但是从技术上讲, typeof也可以扩展为支持字符串文字(包括x具有通用类型的情况):

let x: MyType;
const propName = "abcd";
let y: typeof x[propName];

有了这个扩展,它也可以用来解决一些目标案例:

function get<T>(propName: string, obj: T): typeof obj[propName]

typeof然而更强大,因为它支持无限数量的嵌套级别:

let y: typeof x.prop.childProp.deeperChildProp

而这个只有一个层次。 即(据我所知)没有计划支持:

let y: MyType["prop"]["childProp"]["deeperChildProp"];
// Or alternatively
let y: MyType["prop.childProp.deeperChildProp"];

我认为该提案的范围(如果在模糊的程度下甚至可以称为提案)也太狭窄了。 它可能有助于解决特定问题(尽管可能有替代解决方案),这似乎使许多人非常渴望推广它。 但是,这也消耗了该语言的宝贵语法。 如果没有更广泛的计划和面向设计的方法,那么仓促地介绍到目前为止还没有明确规范的内容似乎是不明智的。

_Edits:更正了代码示例中的一些明显的错误_

我对typeof替代方案进行了一些研究:

已经批准了typeof x["abcd"]typeof x[42]将来语言支持,目前属于#6606,该语言正在开发中(有可行的实现)。

这是成功的一半。 有了这些,其余的可以分几个阶段完成:

(1)添加对字符串文字常量(甚至是数字常量-可能对元组有用吗?)的支持,作为类型表达式中的属性说明符,例如:

let x: MyType;
const propName = "abcd";
let y: typeof x[propName];

(2)允许将这些说明符应用于泛型类型

let x: T; // Where T should extend 'object'
const propName = "abcd";
let y: typeof x[propName];

(3)允许传递这些说明符,并在实践中通过函数参数“实例化”引用(已计划typeof list[0] ,因此我认为这可以涵盖更复杂的情况,例如pluck

function get<T extends object>(obj: T, propertyName: string): typeof obj[propertyName];
function pluck<T extends object>(list: Array<T>, propertyName: string): Array<typeof list[0][propertyName]>;

object类型是在#1809建议的类型)

尽管功能更强大(例如,可能支持typeof obj[propName][nestedPropName] ),但这种替代方法可能无法涵盖此处介绍的所有情况。 请让我知道是否有一些实例似乎无法通过此方法处理(想到的一种情况是根本不传递对象实例,但是,这时我很难想象这种情况尽管我想这是可能的,但这将是必要的。

_编辑:更正了代码中的一些错误_

@malibuzios
一些想法:

  • 这修复了API定义,但对调用者没有帮助。 我们在呼叫站点仍需要某种nameofExpression
  • 它适用于.d.ts定义,但在TS函数/实现中通常不会静态推断甚至无法验证。

我想得越多, Expression<T, U>看起来就越适合这些API。 它可以解决呼叫站点的问题,结果的类型,并且可以在实现中进行推断和验证。

@ jods4

您在前面的注释方式中提到pluck可以用表达式实现。 尽管我曾经非常熟悉C#,但是我从未真正尝试过使用它们,因此我承认我目前对它们还不是很了解。

无论如何,只是想提一下TypeScript支持_type参数inference_ ,所以不必使用pluck ,而可以使用map并获得相同的结果,甚至不需要指定通用参数(推断),并且还会提供完整类型检查和完成:

let x = [{name: "John", age: 34}, {name: "Mary", age: 53}];

let result = x.map(obj => obj.name);
// 'result' is ["John", "Mary"] and its type inferred as 'string[]' 

其中map (为演示而高度简化)声明为

map<U>(mapFunc: (value: T) => U): U[];

此相同的推理模式可用作“技巧”,以将任意lambda的任何结果(甚至不需要调用)传递给函数并为其设置返回类型:

function thisIsATrick<T, U>(obj: T, onlyPassedToInferTheReturnType: () => U): U {
   return;
}

let x = {name: "John", age: 34};

let result = thisIsATrick(x, () => x.age) // Result inferred as 'number' 

_Edit:在这里将其传递给lambda可能看起来很愚蠢,而不仅仅是事情本身! 但是,在更复杂的情况下,例如嵌套对象(例如x.prop.Childprop ),可能存在未定义或空引用,可能会出错。 将其放在不必一定要调用的lambda中可以避免这种情况。

我承认我对这里讨论的一些用例不是很熟悉。 就我个人而言,我从来没有感到需要调用将属性名称作为字符串的函数。 通常将lambda(您所描述的优点也保留在其中)与类型实参接口结合使用足以解决许多常见问题。

我之所以建议typeof替代方法,主要是因为这似乎涵盖了相同的用例,并为人们在此处描述的内容提供了相同的功能。 我会自己使用吗? 我不知道,从来没有真正的需要(这可能是我几乎从未使用过下划线这样的外部库,我几乎总是在需要的时候自己开发实用程序函数)。

@malibuzios是的, pluck有点傻,它内置了lambdas + map

在此注释中,我提供了5个不同的API,它们都带有字符串-它们都与观察更改有关。

@ jods4

只是想提一下,当#6606完成时,仅实现第1阶段(允许将常量字符串文字用作属性说明符)就可以实现此处所需的某些功能,尽管并不尽如人意(可能需要属性名称,将其声明为常量,并添加其他语句)。

function observeProperty<T, U>(obj: T, propName: string ): Subscriber<U> {
    ....
}

let x = { name: "John", age: 42 };

const propName = "age";
observeProperty<typeof x, typeof x[propName]>(x, propName);

但是,实现此目标所需的工作量也可能远低于实施阶段2和3(我不确定,但是#6606可能已经覆盖了其中的1)。 在实施第2阶段后,这还将涵盖x具有泛型类型的情况(但是我不确定这是否真的需要吗?)。

编辑:我使用外部常量而不只是两次写属性名称的原因不仅是为了减少键入,而且还确保名称始终匹配,尽管这仍然不能与“重命名”和“查找全部”之类的工具一起使用参考资料”,我认为这是一个严重的劣势。

@ jods4我仍在寻找一种不使用字符串的更好的解决方案。 我将尝试考虑使用nameof及其变体可以做什么。

这是另一个想法。

由于字符串文字已经作为类型被支持,因此例如可以编写:

let x: "HELLO"; 

可以将传递给函数的文字字符串视为通用专业化形式

(_Edit:这些示例已得到纠正,以确保s在函数主体中不可变,此时参数位置尚不支持const (不确定readonly虽然)._):

function func(const s: string)

s关联的string参数类型可以在此处视为隐式泛型(因为它可以专用于字符串文字)。 为了清楚起见,我将更明确地编写它(尽管我不确定是否确实需要这样做):

function func<S extends string>(const s: S)
func("TEST");

可以在内部专门研究:

function func(s: "TEST")

现在,这可以用作“传递”属性名称的另一种方法,我觉得这里可以更好地捕获语义。

function observeProperty<T, S extends string>(obj: T, const propName: S): Subscriber<T[S]>

x = { name: "John",  age: 33};

observeProperty(x, nameof(x.age))

由于在T[S]TS都是通用参数。 与混合运行时元素和类型作用域元素(例如T[someString] )相比,将两种类型组合在一个类型位置中似乎更自然。

_Edits:重写的示例具有不可变的字符串参数类型。

由于我使用的是TS 1.8.7而不是最新版本,因此我不知道在最新版本中

const x = "Hello";

x类型已经被推断为文字类型"Hello" ,即x: "Hello" ,这很有意义(请参阅#6554)。

因此很自然地,如果有一种方法可以将参数定义为const (或者也许readonly也可以?):

function func<S extends string>(const s: S): S

那么我相信这可以成立:

let result = func("abcd"); // type of 'result' inferred as the literal type "abcd"

由于其中一些是相当新的,并且依赖于最新的语言功能,因此,我将尽力将其总结得尽可能清楚:

(1)当const (也许还有readonly ?)字符串变量接收到一个在编译时已知的值时,该变量会自动接收一个具有相同值的文字类型(我相信这是最近的行为,不会在1.8.x )上发生,例如

const x = "ABCD";

x类型推断为"ABCD" ,而不是string !,例如,可以将其声明为x: "ABCD"

(2)如果允许readonly函数参数,则带有泛型类型的readonly参数自然会在将其作为参数接收时将其类型专门化为字符串文字,因为该参数是不可变的!

function func<S extends string>(readonly str: S);
func("ABCD");

这里S已解析为类型"ABCD" ,而不是string

但是,如果str参数不是不可变的,则编译器无法保证不会在函数主体中重新分配该参数,因此,推断的类型仅为string

function func<S extends string>(str: S) {
    str = "DCBA"; // This may happen
}

func("ABCD");

(3)可以利用这一点并修改原始提案,以使属性说明符可以引用类型,该string的派生类(在某些情况下,将其限制为仅单例文字类型可能更合适,尽管目前尚无真正的方法,而不是运行时实体:

function get<T extends object, S extends string>(obj: T, readonly propName: S): T[S]

调用此命令不需要显式指定任何类型参数,因为TypeScript支持类型参数推断:

let x = { name: "John", age: 42 };

get(x, "age"); // result type is inferred to be 'number'
// or for stronger type safety:
get(x, nameof(x.age)); // result type is inferred to be 'number'

_编辑:更正了一些拼写和代码错误。
_注意:此修改方法的通用和扩展版本现在也已在#7730中跟踪。_

这是在@Raynos的讨论中出现的类型属性类型(或我最近喜欢称呼它们为“索引泛型”)的另一种用法

我们已经可以为数组编写以下常规检查器:

function tArray<T>(f:(t:any) => t is T) {
    return function (a:any): a is Array<T> {
        if (!Array.isArray(a)) return false;
        for (var k = 0; k < a.length; ++k)
            if (!f(a[k])) return false;
        return true;
    }
}

function tNumber(n:any): n is number {
    return typeof n === 'number'
}
var isArrayOfNumber = tArray(tNumber)

function test(x: {}) {
    if (isArrayOfNumber(x)) {
        return x[x.length - 1].toFixed(2); // this type checks
    }
}

一旦索引了泛型,我们还可以编写对象的常规检查器:

function tObject<p, T[p]>(checker: {...p: (t:any) => t is T[p]}) {
  return function(obj: any): obj is {...p: T[p] } {
    for (var key in checker) if (!checker[key](obj[key])) return false;
    return true;
  }
}

它与字符串,数字,布尔值,null和undefined的原始检查器一起使您可以编写如下内容:

var isTodoList = tObject({
  items: tArray(tObject({text: tString, completed: tBoolean})),
  showCompleted: tBoolean
})

并同时使用正确的运行时检查器_和_编译时类型防护来获得结果:)

有没有人对此做过任何工作,还是有人在做雷达? 这将是对多少个标准库可以使用类型的一个重大改进,包括lodashramda以及许多数据库接口。

@malibuzios我相信您正在接近@Artazor的建议:)

要解决您的问题:

function func<S extends string>(readonly str: S): T[str] {
 ...
}

这是

function func<S extends string, T[S]>(str: S):T[S] { }

这样,使用以下命令调用时,名称将被锁定为最特定的类型(字符串常量类型):

func("test")

类型S变为"test" (而不是string )。 因此,无法将str重新分配为其他值。 如果您尝试过,例如

str = "other"

编译器可能会产生错误(除非存在方差健全性问题;)

我不仅可以选择一个属性的类型,还可以选择获取任意的超级类型。

因此,我的建议是添加以下语法:我不想添加T[prop] ,而是要添加语法T[...props] 。 其中props必须是T的成员数组。 将得到的类型是超类型的T与成员T所定义props

我认为这对Sequelize(一种流行的node.js ORM)非常有用。 出于安全性和性能方面的考虑,明智的做法是仅查询需要使用的表中的属性。 这通常意味着一种类型的超级类型。

interface IUser {
    id: string;
    name: string;
    email: string;
    createdAt: string;
    updatedAt: string;
    password: string;
    // ...
}

interface Options<T> {
     attributes: (memberof T)[];
}

interface Model<IInstance> {
     findOne(options: Options<IInstance>): IInstance[...options.attributes];
}

declare namespace DbContext {
   define<T>(): Model<T>;
}

const Users = DbContext.define<IUser>({
   id: { type: DbContext.STRING(50), allowNull: false },
   // ...
});

const user = Users.findOne({
    attributes: ['id', 'email', 'name'],
    where: {
        id: 1,
    }
});

user.id
user.email
user.name

user.password // error
user.createdAt // error
user.updatedAt // error

(在我的示例中,它包括运算符memberof ,这是您期望的,以及表达式options.attributestypeof options.attributes ,但我认为typeof运算符在这种情况下是多余的,因为它处于期望类型的位置。)

如果没有人坚持,我将开始从事这一工作。

您对函数内部的类型安全有何想法,即确保ret​​urn语句返回可分配给返回类型的内容?

interface A {
     a: string;
}
function f(p: string): A[p] {
    return 'aaa'; // This is string, but can we ensure it is the intended A[p] ?
}

同样,此处使用的名称“属性类型”似乎有点不正确。 某种状态表明该类型具有几乎所有类型都具有的属性。

那“属性引用类型”呢?

您对函数内部的类型安全有何看法

我的头脑风暴:

let a: A;
function f(p: string): A[p] {
  let x = a[p]; // typeof A[p], only when:
  // 1. p is directly referencing function argument
  // 2. function return type is Property Reference Type

  p = "abc"; // not allowed to assign a new value when p is used on Property Reference Type

  return x; // x is A[p], so okay
}

并禁止在返回行上使用普通字符串。

@malibuzios您的想法的问题是,它需要指定所有泛型,以防无法推断出它们,这可能会病毒式传播/污染代码库。

TS小组对https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -239653337有何评论?

@RyanCavanaugh @mhegazy等?

关于名称,我开始将此功能(至少是@Artazor提出的形式)

从另一个角度来看,解决方案可能是针对此问题的。 我不确定它是否已经带来,这是一个长线程。 开发一个字符串通用建议,我们可以扩展索引签名。 由于字符串文字可以用于索引器类型,因此我们可以使它们等效(据我所知,它们现在不行):

interface A1 {
    a: number;
    b: boolean;
}
interface A2 {
    [index: "a"]: number;
    [index: "b"]: boolean;
}

所以,我们可以写

declare function pluck<P, T extends { [indexer: P]: R; }, R>(obj: T, p: P): R;

有几件事需要考虑:

  • 如何指示P只能是字符串文字?

    • 由于字符串在技术上扩展了所有字符串文字,因此P extends string并不是一个很理想的概念

    • 关于允许P super string约束的其他讨论正在进行中(#7265,#6613,https://github.com/Microsoft/TypeScript/issues/6613#issuecomment-175314703)

    • 我们真的需要关心这个吗? 我们可以使用任何可接受的索引器类型-如果T的索引器为string s或number s。 那么P可以是stringnumber

  • 当前,如果我们将"something"作为第二个参数传递,则其类型string

    • 字符串文字#10195可能是答案

    • 或TS可以推断是否可以使用更具体的P

  • 索引器是隐式可选的{ [i: string]: number /* | undefined */ }

    • 如果我们真的不想在域中拥有undefined ,该如何执行?

  • PTR之间的所有关系的自动类型推断是使其工作的关键

@weswigham @mhegazy ,最近我一直在讨论这个问题; 我们将让您知道我们遇到的任何发展,并且请记住,这只是构思的原型。

目前的想法:

  • keysof Foo运算符从Foo作为字符串文字类型获取属性名称的并集。
  • Foo[K]类型,它指定对于某些类型K是字符串文字类型或字符串文字类型的并集。

从这些基本块中,如果您需要将字符串文字推断为适当的类型,则可以编写

function foo<T, K extends keysof T>(obj: T, key: K): T[K] {
    // ...
}

在这里, K将是keysof T的子类型,这意味着它是字符串文字类型或字符串文字类型的并集。 您为key参数传递的所有内容均应由该文字/文字统一体在上下文中键入,并推断为单例字符串文字类型。

例如

interface HelloWorld { hello: any; world: any; }

function foo<K extends keysof HelloWorld>(key: K): K {
    return key;
}

// 'x' has type '"hello"'
let x = foo("hello");

最大的问题是keysof通常需要“延迟”其操作。 它太急于如何评估类型,这对类型参数是一个问题,例如我发布的第一个示例(即,我们真正要解决的情况实际上是困难的部分:smile :)。

希望能给大家带来更新。

@DanielRosenwasser感谢您的更新。 我刚刚看到@weswigham提交了有关keysof运算符的PR,所以最好将此问题交给你们。

我只是想知道为什么您决定脱离最初提出的语法?

function get(prop: string): T[prop];

并介绍keysof

T[prop]不太通用,需要大量的隔行扫描机械。 这里的一个大问题是,你怎么连相关的文字内容prop来的属性名称T 。 我什至不完全确定你会做什么。 您会添加隐式类型参数吗? 您是否需要更改上下文键入行为? 您需要为签名添加一些特殊的东西吗?

对于所有这些事情,答案可能是肯定的。 我之所以放弃,是因为我的直觉告诉我,最好使用两个更简单,独立的概念并从那里开始。 不利的一面是在某些情况下会有更多样板。

如果有较新的库使用这些类型的模式,而样板使它们很难用TypeScript编写,那么也许我们应该考虑一下。 但是总的来说,此功能主要是为了服务于图书馆用户,因为无论如何,使用站点都是您可以从中受益的地方。

@DanielRosenwasser几乎没有@SaschaNaz的想法吗? 我认为keysof在这种情况下是多余的。 T[p]已经认为p必须是T的字面道具之一。

我粗略的实现思想是引入一种称为PropertyReferencedType的新类型。

export interface PropertyReferencedType extends Type {
        property: Symbol;
        targetType: ObjectType;
}

当输入声明为PropertyReferencedType的返回类型的函数或输入引用PropertyReferencedType的函数时: ElementAccessExpression将使用引用了被访问属性的符号。

export interface Type {
        flags: TypeFlags;                // Flags
        /* <strong i="20">@internal</strong> */ id: number;      // Unique ID
        //...
        referencedProperty: Symbol; // referenced property
}

因此,具有引用的属性符号的类型可分配给PropertyReferencedType 。 在检查中, referencedProperty必须对应于pT[p] 。 同样,元素访问表达式的父类型必须可分配给T 。 为了使事情变得容易, p也必须是const。

新类型PropertyReferencedType仅在函数内部以“未解析类型”存在。 在呼叫站点上,必须使用p解析类型:

interface A { a: string }
declare function getProp(p: string): A[p]
getProp('a'); // string

PropertyReferencedType仅通过函数分配传播,而不能通过调用表达式传播,因为PropertyReferencedType只是一个临时类型,旨在帮助检查带有返回类型T[p]的函数的主体

如果引入keysofT[K]类型的运算符,是否意味着我们可以这样使用它们:

interface A {
  a: number;
  b: string;
}
type AK = keysof A; // "a" | "b"
type AV = A[AK]; // number | string ?
type AA = A["a"]; // number ?
type AB = A["b"]; // string ?
type AC = A["c"]; // error?
type AN = A[number]; // error?

type X1 = keysof { [index: string]: number; }; // string ?
type X2 = keysof { [index: string]: number; [index: number]: string; }; // string | number ?

@DanielRosenwasser你的例子与我的意思不一样

function foo<T, K extends keysof T>(obj: T, key: K): T[K] {
    // ...
}
// same as ?
function foo<K, V, T extends { [k: K]: V; }>(obj: T, key: K): V {
    // ...
}

我看不到Underscore的_.pick的签名将如何写入:

o2 = _.pick(o1, 'p1', 'p2');

pick(Object, ...props: String[]) : WHAT GOES HERE;

@rtm我在https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -234724380中建议了它。 尽管打开一个新的问题可能与此有关,但最好打开一个新的问题。

现在可在#11929中实现。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

MartynasZilinskas picture MartynasZilinskas  ·  3评论

fwanicka picture fwanicka  ·  3评论

Antony-Jones picture Antony-Jones  ·  3评论

uber5001 picture uber5001  ·  3评论

manekinekko picture manekinekko  ·  3评论